From 1ec5bf40627e0f77110add0fac7fb2c7e7063c87 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 17:43:37 +0000 Subject: [PATCH] feat(api): attach request meta to error envelopes Co-Authored-By: Virgil --- middleware_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ response_meta.go | 8 +++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/middleware_test.go b/middleware_test.go index 558fd8d..6bcaa9d 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -73,6 +73,17 @@ func (g autoResponseMetaTestGroup) RegisterRoutes(rg *gin.RouterGroup) { }) } +type autoErrorResponseMetaTestGroup struct{} + +func (g autoErrorResponseMetaTestGroup) Name() string { return "auto-error-response-meta" } +func (g autoErrorResponseMetaTestGroup) BasePath() string { return "/v1" } +func (g autoErrorResponseMetaTestGroup) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/error", func(c *gin.Context) { + time.Sleep(2 * time.Millisecond) + c.JSON(http.StatusBadRequest, api.Fail("bad_request", "request failed")) + }) +} + // ── Bearer auth ───────────────────────────────────────────────────────── func TestBearerAuth_Bad_MissingToken(t *testing.T) { @@ -310,6 +321,42 @@ func TestResponseMeta_Good_AttachesMetaAutomatically(t *testing.T) { } } +func TestResponseMeta_Good_AttachesMetaToErrorResponses(t *testing.T) { + gin.SetMode(gin.TestMode) + e, _ := api.New( + api.WithRequestID(), + api.WithResponseMeta(), + ) + e.Register(autoErrorResponseMetaTestGroup{}) + + h := e.Handler() + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/v1/error", nil) + req.Header.Set("X-Request-ID", "client-id-auto-error-meta") + h.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", w.Code) + } + + 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-auto-error-meta" { + t.Fatalf("expected request_id=%q, got %q", "client-id-auto-error-meta", resp.Meta.RequestID) + } + if resp.Meta.Duration == "" { + t.Fatal("expected duration to be populated") + } + if resp.Error == nil || resp.Error.Code != "bad_request" { + t.Fatalf("expected bad_request error, got %+v", resp.Error) + } +} + // ── CORS ──────────────────────────────────────────────────────────────── func TestCORS_Good_PreflightAllOrigins(t *testing.T) { diff --git a/response_meta.go b/response_meta.go index c17b6e0..5ff1d2f 100644 --- a/response_meta.go +++ b/response_meta.go @@ -104,8 +104,8 @@ func (w *responseMetaRecorder) commit() { _, _ = w.ResponseWriter.Write(w.body.Bytes()) } -// responseMetaMiddleware injects request metadata into successful JSON -// envelope responses before they are written to the client. +// 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 { @@ -156,7 +156,9 @@ func refreshResponseMetaBody(body []byte, meta *Meta) []byte { } if _, ok := obj["success"]; !ok { - return body + if _, ok := obj["error"]; !ok { + return body + } } current := map[string]any{}