From d7ef3610f7e408246c294b4cc2f4f4a8709a4d17 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 03:38:34 +0000 Subject: [PATCH] fix(response): attach meta to all json responses Co-Authored-By: Virgil --- middleware_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++ response_meta.go | 19 +++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/middleware_test.go b/middleware_test.go index 6bcaa9d..58d9c43 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -84,6 +84,18 @@ func (g autoErrorResponseMetaTestGroup) RegisterRoutes(rg *gin.RouterGroup) { }) } +type plusJSONResponseMetaTestGroup struct{} + +func (g plusJSONResponseMetaTestGroup) Name() string { return "plus-json-response-meta" } +func (g plusJSONResponseMetaTestGroup) BasePath() string { return "/v1" } +func (g plusJSONResponseMetaTestGroup) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/plus-json", func(c *gin.Context) { + c.Header("Content-Type", "application/problem+json") + c.Status(http.StatusOK) + _, _ = c.Writer.Write([]byte(`{"success":true,"data":"ok"}`)) + }) +} + // ── Bearer auth ───────────────────────────────────────────────────────── func TestBearerAuth_Bad_MissingToken(t *testing.T) { @@ -357,6 +369,43 @@ func TestResponseMeta_Good_AttachesMetaToErrorResponses(t *testing.T) { } } +func TestResponseMeta_Good_AttachesMetaToPlusJSONContentType(t *testing.T) { + gin.SetMode(gin.TestMode) + e, _ := api.New( + api.WithRequestID(), + api.WithResponseMeta(), + ) + e.Register(plusJSONResponseMetaTestGroup{}) + + h := e.Handler() + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/v1/plus-json", nil) + req.Header.Set("X-Request-ID", "client-id-plus-json-meta") + h.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + if got := w.Header().Get("Content-Type"); got != "application/problem+json" { + t.Fatalf("expected Content-Type to be preserved, got %q", got) + } + + var resp api.Response[string] + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if resp.Meta == nil { + t.Fatal("expected Meta to be present") + } + if resp.Meta.RequestID != "client-id-plus-json-meta" { + t.Fatalf("expected request_id=%q, got %q", "client-id-plus-json-meta", resp.Meta.RequestID) + } + if resp.Meta.Duration == "" { + t.Fatal("expected duration to be populated") + } +} + // ── CORS ──────────────────────────────────────────────────────────────── func TestCORS_Good_PreflightAllOrigins(t *testing.T) { diff --git a/response_meta.go b/response_meta.go index 5ff1d2f..0ca66bd 100644 --- a/response_meta.go +++ b/response_meta.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "io" + "mime" "net" "net/http" "strconv" @@ -184,10 +185,26 @@ func refreshResponseMetaBody(body []byte, meta *Meta) []byte { } func shouldAttachResponseMeta(contentType string, body []byte) bool { - if !strings.Contains(contentType, "application/json") { + if !isJSONContentType(contentType) { return false } trimmed := bytes.TrimSpace(body) return len(trimmed) > 0 && trimmed[0] == '{' } + +func isJSONContentType(contentType string) bool { + if strings.TrimSpace(contentType) == "" { + return false + } + + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + mediaType = strings.TrimSpace(contentType) + } + mediaType = strings.ToLower(mediaType) + + return mediaType == "application/json" || + strings.HasSuffix(mediaType, "+json") || + strings.HasSuffix(mediaType, "/json") +}