Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
287 lines
5.6 KiB
Go
287 lines
5.6 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"mime"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// responseMetaRecorder buffers JSON responses so request metadata can be
|
|
// injected into the standard envelope before the body is written to the client.
|
|
type responseMetaRecorder struct {
|
|
gin.ResponseWriter
|
|
headers http.Header
|
|
body bytes.Buffer
|
|
size int
|
|
status int
|
|
wroteHeader bool
|
|
committed bool
|
|
passthrough bool
|
|
}
|
|
|
|
func newResponseMetaRecorder(w gin.ResponseWriter) *responseMetaRecorder {
|
|
headers := make(http.Header)
|
|
for k, vals := range w.Header() {
|
|
headers[k] = append([]string(nil), vals...)
|
|
}
|
|
|
|
return &responseMetaRecorder{
|
|
ResponseWriter: w,
|
|
headers: headers,
|
|
status: http.StatusOK,
|
|
}
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Header() http.Header {
|
|
return w.headers
|
|
}
|
|
|
|
func (w *responseMetaRecorder) WriteHeader(code int) {
|
|
if w.passthrough {
|
|
w.status = code
|
|
w.wroteHeader = true
|
|
w.ResponseWriter.WriteHeader(code)
|
|
return
|
|
}
|
|
w.status = code
|
|
w.wroteHeader = true
|
|
}
|
|
|
|
func (w *responseMetaRecorder) WriteHeaderNow() {
|
|
if w.passthrough {
|
|
w.wroteHeader = true
|
|
w.ResponseWriter.WriteHeaderNow()
|
|
return
|
|
}
|
|
w.wroteHeader = true
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Write(data []byte) (int, error) {
|
|
if w.passthrough {
|
|
if !w.wroteHeader {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
return w.ResponseWriter.Write(data)
|
|
}
|
|
if !w.wroteHeader {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
n, err := w.body.Write(data)
|
|
w.size += n
|
|
return n, err
|
|
}
|
|
|
|
func (w *responseMetaRecorder) WriteString(s string) (int, error) {
|
|
if w.passthrough {
|
|
if !w.wroteHeader {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
return w.ResponseWriter.WriteString(s)
|
|
}
|
|
if !w.wroteHeader {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
n, err := w.body.WriteString(s)
|
|
w.size += n
|
|
return n, err
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Flush() {
|
|
if w.passthrough {
|
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
return
|
|
}
|
|
|
|
if !w.wroteHeader {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
w.commit(true)
|
|
w.passthrough = true
|
|
|
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Status() int {
|
|
if w.wroteHeader {
|
|
return w.status
|
|
}
|
|
|
|
return http.StatusOK
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Size() int {
|
|
if w.passthrough {
|
|
return w.ResponseWriter.Size()
|
|
}
|
|
return w.size
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Written() bool {
|
|
return w.wroteHeader
|
|
}
|
|
|
|
func (w *responseMetaRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
if w.passthrough {
|
|
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
|
|
return h.Hijack()
|
|
}
|
|
return nil, nil, io.ErrClosedPipe
|
|
}
|
|
|
|
w.wroteHeader = true
|
|
w.passthrough = true
|
|
|
|
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
|
|
return h.Hijack()
|
|
}
|
|
return nil, nil, io.ErrClosedPipe
|
|
}
|
|
|
|
func (w *responseMetaRecorder) commit(writeBody bool) {
|
|
if w.committed {
|
|
return
|
|
}
|
|
|
|
for k := range w.ResponseWriter.Header() {
|
|
w.ResponseWriter.Header().Del(k)
|
|
}
|
|
|
|
for k, vals := range w.headers {
|
|
for _, v := range vals {
|
|
w.ResponseWriter.Header().Add(k, v)
|
|
}
|
|
}
|
|
|
|
w.ResponseWriter.WriteHeader(w.Status())
|
|
if writeBody {
|
|
_, _ = w.ResponseWriter.Write(w.body.Bytes())
|
|
w.body.Reset()
|
|
}
|
|
w.committed = true
|
|
}
|
|
|
|
// responseMetaMiddleware injects request metadata into JSON envelope
|
|
// responses before they are written to the client.
|
|
func responseMetaMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if _, ok := c.Get(requestStartContextKey); !ok {
|
|
c.Set(requestStartContextKey, time.Now())
|
|
}
|
|
|
|
recorder := newResponseMetaRecorder(c.Writer)
|
|
c.Writer = recorder
|
|
|
|
c.Next()
|
|
|
|
if recorder.passthrough {
|
|
return
|
|
}
|
|
|
|
body := recorder.body.Bytes()
|
|
if meta := GetRequestMeta(c); meta != nil && shouldAttachResponseMeta(recorder.Header().Get("Content-Type"), body) {
|
|
if refreshed := refreshResponseMetaBody(body, meta); refreshed != nil {
|
|
body = refreshed
|
|
}
|
|
}
|
|
|
|
recorder.body.Reset()
|
|
_, _ = recorder.body.Write(body)
|
|
recorder.size = len(body)
|
|
recorder.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
|
recorder.commit(true)
|
|
}
|
|
}
|
|
|
|
// refreshResponseMetaBody injects request metadata into a cached or buffered
|
|
// JSON envelope without disturbing existing pagination metadata.
|
|
func refreshResponseMetaBody(body []byte, meta *Meta) []byte {
|
|
if meta == nil {
|
|
return body
|
|
}
|
|
|
|
var payload any
|
|
dec := json.NewDecoder(bytes.NewReader(body))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(&payload); err != nil {
|
|
return body
|
|
}
|
|
|
|
var extra any
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
|
return body
|
|
}
|
|
|
|
obj, ok := payload.(map[string]any)
|
|
if !ok {
|
|
return body
|
|
}
|
|
|
|
if _, ok := obj["success"]; !ok {
|
|
if _, ok := obj["error"]; !ok {
|
|
return body
|
|
}
|
|
}
|
|
|
|
current := map[string]any{}
|
|
if existing, ok := obj["meta"].(map[string]any); ok {
|
|
current = existing
|
|
}
|
|
|
|
if meta.RequestID != "" {
|
|
current["request_id"] = meta.RequestID
|
|
}
|
|
if meta.Duration != "" {
|
|
current["duration"] = meta.Duration
|
|
}
|
|
|
|
obj["meta"] = current
|
|
|
|
updated, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return body
|
|
}
|
|
|
|
return updated
|
|
}
|
|
|
|
func shouldAttachResponseMeta(contentType string, body []byte) bool {
|
|
if !isJSONContentType(contentType) {
|
|
return false
|
|
}
|
|
|
|
trimmed := bytes.TrimSpace(body)
|
|
return len(trimmed) > 0 && trimmed[0] == '{'
|
|
}
|
|
|
|
func isJSONContentType(contentType string) bool {
|
|
if core.Trim(contentType) == "" {
|
|
return false
|
|
}
|
|
|
|
mediaType, _, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
mediaType = core.Trim(contentType)
|
|
}
|
|
mediaType = core.Lower(mediaType)
|
|
|
|
return mediaType == "application/json" ||
|
|
core.HasSuffix(mediaType, "+json") ||
|
|
core.HasSuffix(mediaType, "/json")
|
|
}
|