From 1bc2cfb507ceb6aec0d6c057201d2706874951cc Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Fri, 13 Feb 2026 11:20:15 +0900 Subject: [PATCH] feat(backend): backfill error code on legacy error responses --- backend/cmd/server/main.go | 3 + .../middleware/error_code_enricher.go | 49 ++++++++++ .../middleware/error_code_enricher_test.go | 91 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 backend/internal/middleware/error_code_enricher.go create mode 100644 backend/internal/middleware/error_code_enricher_test.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ccf938e5..7ed87e7e 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -317,6 +317,9 @@ func main() { EnableStackTrace: true, })) + // Backfill `code` on legacy JSON error responses during migration period. + app.Use(middleware.ErrorCodeEnricher()) + allowedOrigins := getEnv("CORS_ALLOWED_ORIGINS", "http://localhost:5000") allowCredentials := allowedOrigins != "*" app.Use(cors.New(cors.Config{ diff --git a/backend/internal/middleware/error_code_enricher.go b/backend/internal/middleware/error_code_enricher.go new file mode 100644 index 00000000..5d7c1813 --- /dev/null +++ b/backend/internal/middleware/error_code_enricher.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "baron-sso-backend/internal/response" + "encoding/json" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// ErrorCodeEnricher injects machine-readable `code` into legacy error responses. +// This keeps backward compatibility (`error` stays) while migrating handlers +// incrementally to explicit code-based responses. +func ErrorCodeEnricher() fiber.Handler { + return func(c *fiber.Ctx) error { + err := c.Next() + + body := c.Response().Body() + if len(body) == 0 { + return err + } + + contentType := string(c.Response().Header.ContentType()) + if contentType != "" && !strings.Contains(contentType, fiber.MIMEApplicationJSON) { + return err + } + + var payload map[string]any + if unmarshalErr := json.Unmarshal(body, &payload); unmarshalErr != nil { + return err + } + + if _, hasError := payload["error"]; !hasError { + return err + } + if _, hasCode := payload["code"]; hasCode { + return err + } + + payload["code"] = response.StatusCode(c.Response().StatusCode()) + updated, marshalErr := json.Marshal(payload) + if marshalErr != nil { + return err + } + + c.Response().SetBodyRaw(updated) + return err + } +} diff --git a/backend/internal/middleware/error_code_enricher_test.go b/backend/internal/middleware/error_code_enricher_test.go new file mode 100644 index 00000000..ba0e65b0 --- /dev/null +++ b/backend/internal/middleware/error_code_enricher_test.go @@ -0,0 +1,91 @@ +package middleware + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" +) + +func decodeBody(t *testing.T, resp *http.Response) map[string]any { + t.Helper() + + var body map[string]any + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode body: %v", err) + } + return body +} + +func TestErrorCodeEnricher_AddsCodeToLegacyErrorResponse(t *testing.T) { + app := fiber.New() + app.Use(ErrorCodeEnricher()) + app.Get("/legacy", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "missing token", + }) + }) + + req := httptest.NewRequest(http.MethodGet, "/legacy", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + + if resp.StatusCode != fiber.StatusUnauthorized { + t.Fatalf("unexpected status code: %d", resp.StatusCode) + } + + body := decodeBody(t, resp) + if body["error"] != "missing token" { + t.Fatalf("unexpected error field: %v", body["error"]) + } + if body["code"] != "invalid_session" { + t.Fatalf("unexpected code: %v", body["code"]) + } +} + +func TestErrorCodeEnricher_DoesNotOverrideExistingCode(t *testing.T) { + app := fiber.New() + app.Use(ErrorCodeEnricher()) + app.Get("/already", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid request", + "code": "validation_format", + }) + }) + + req := httptest.NewRequest(http.MethodGet, "/already", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + + body := decodeBody(t, resp) + if body["code"] != "validation_format" { + t.Fatalf("code should not be overridden: %v", body["code"]) + } +} + +func TestErrorCodeEnricher_IgnoreSuccessPayload(t *testing.T) { + app := fiber.New() + app.Use(ErrorCodeEnricher()) + app.Get("/ok", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "message": "ok", + }) + }) + + req := httptest.NewRequest(http.MethodGet, "/ok", nil) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + + body := decodeBody(t, resp) + if _, found := body["code"]; found { + t.Fatalf("code should not be injected for success payload") + } +}