diff --git a/options.go b/options.go index a54d562..c75b304 100644 --- a/options.go +++ b/options.go @@ -119,8 +119,9 @@ func WithAuthentik(cfg AuthentikConfig) Option { } // WithSunset adds deprecation headers to every response. -// The middleware emits Deprecation, optional Sunset, optional Link, and -// X-API-Warn headers. Use it to deprecate an entire route group or API version. +// The middleware appends Deprecation, optional Sunset, optional Link, and +// X-API-Warn headers without clobbering any existing header values. Use it to +// deprecate an entire route group or API version. func WithSunset(sunsetDate, replacement string) Option { return func(e *Engine) { e.middlewares = append(e.middlewares, ApiSunset(sunsetDate, replacement)) diff --git a/sunset.go b/sunset.go index a8b17e5..24d2709 100644 --- a/sunset.go +++ b/sunset.go @@ -12,8 +12,10 @@ import ( // ApiSunset returns middleware that marks a route or group as deprecated. // -// The middleware adds standard deprecation headers to every response: -// Deprecation, optional Sunset, optional Link, and X-API-Warn. +// The middleware appends standard deprecation headers to every response: +// Deprecation, optional Sunset, optional Link, and X-API-Warn. Existing header +// values are preserved so downstream middleware and handlers can keep their own +// link relations or warning metadata. // // Example: // @@ -30,14 +32,14 @@ func ApiSunset(sunsetDate, replacement string) gin.HandlerFunc { return func(c *gin.Context) { c.Next() - c.Header("Deprecation", "true") + c.Writer.Header().Add("Deprecation", "true") if formatted != "" { - c.Header("Sunset", formatted) + c.Writer.Header().Add("Sunset", formatted) } if replacement != "" { c.Writer.Header().Add("Link", "<"+replacement+">; rel=\"successor-version\"") } - c.Header("X-API-Warn", warning) + c.Writer.Header().Add("X-API-Warn", warning) } } diff --git a/sunset_test.go b/sunset_test.go index 78637ae..1348be7 100644 --- a/sunset_test.go +++ b/sunset_test.go @@ -33,6 +33,20 @@ func (sunsetLinkStubGroup) RegisterRoutes(rg *gin.RouterGroup) { }) } +type sunsetHeaderStubGroup struct{} + +func (sunsetHeaderStubGroup) Name() string { return "legacy-headers" } +func (sunsetHeaderStubGroup) BasePath() string { return "/legacy-headers" } +func (sunsetHeaderStubGroup) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/status", func(c *gin.Context) { + c.Header("Deprecation", "false") + c.Header("Sunset", "Wed, 01 Jan 2025 00:00:00 GMT") + c.Header("X-API-Warn", "Existing warning") + c.Header("Link", "; rel=\"help\"") + c.JSON(http.StatusOK, api.OK("ok")) + }) +} + func TestWithSunset_Good_AddsDeprecationHeaders(t *testing.T) { gin.SetMode(gin.TestMode) @@ -91,3 +105,34 @@ func TestWithSunset_Good_PreservesExistingLinkHeaders(t *testing.T) { t.Fatalf("expected successor Link header to be appended, got %q", links[1]) } } + +func TestWithSunset_Good_PreservesExistingDeprecationHeaders(t *testing.T) { + gin.SetMode(gin.TestMode) + + e, err := api.New(api.WithSunset("2025-06-01", "/api/v2/status")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + e.Register(sunsetHeaderStubGroup{}) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/legacy-headers/status", nil) + e.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + if got := w.Header().Values("Deprecation"); len(got) != 2 { + t.Fatalf("expected 2 Deprecation header values, got %v", got) + } + if got := w.Header().Values("Sunset"); len(got) != 2 { + t.Fatalf("expected 2 Sunset header values, got %v", got) + } + if got := w.Header().Values("X-API-Warn"); len(got) != 2 { + t.Fatalf("expected 2 X-API-Warn header values, got %v", got) + } + if got := w.Header().Values("Link"); len(got) != 2 { + t.Fatalf("expected 2 Link header values, got %v", got) + } +}