From 490a3741a1211bc8322290eb06103a7351b8c58c Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 16 Apr 2026 05:40:17 -0700 Subject: [PATCH] fix(test): wrap httptest.ResponseRecorder with CloseNotify for canvas proxy tests httputil.ReverseProxy calls CloseNotify() which httptest.ResponseRecorder doesn't implement. Gin casts the writer, causing a panic. Added a closeNotifyRecorder wrapper with a no-op channel. --- platform/internal/router/canvas_proxy_test.go | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/platform/internal/router/canvas_proxy_test.go b/platform/internal/router/canvas_proxy_test.go index d85152b5..6f597043 100644 --- a/platform/internal/router/canvas_proxy_test.go +++ b/platform/internal/router/canvas_proxy_test.go @@ -8,14 +8,23 @@ import ( "github.com/gin-gonic/gin" ) -// TestCanvasProxy_StripsAuthorizationHeader verifies that workspace bearer -// tokens are NOT forwarded to the canvas Next.js server (issue #451 / N2). -// A compromised or unpatched Next.js route could echo the token back to an -// attacker; stripping it at the proxy layer is the safe default. +// closeNotifyRecorder wraps httptest.ResponseRecorder with a no-op +// CloseNotify so httputil.ReverseProxy doesn't panic when served +// through Gin (which casts the writer to http.CloseNotifier). +type closeNotifyRecorder struct { + *httptest.ResponseRecorder +} + +func (c *closeNotifyRecorder) CloseNotify() <-chan bool { + return make(chan bool) +} + +func newTestRecorder() *closeNotifyRecorder { + return &closeNotifyRecorder{httptest.NewRecorder()} +} + func TestCanvasProxy_StripsAuthorizationHeader(t *testing.T) { var capturedAuth string - - // Stand up a tiny upstream that records what headers it received. upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedAuth = r.Header.Get("Authorization") w.WriteHeader(http.StatusOK) @@ -26,7 +35,7 @@ func TestCanvasProxy_StripsAuthorizationHeader(t *testing.T) { engine := gin.New() engine.NoRoute(newCanvasProxy(upstream.URL)) - w := httptest.NewRecorder() + w := newTestRecorder() req := httptest.NewRequest("GET", "/some-canvas-page", nil) req.Header.Set("Authorization", "Bearer ws-secret-token") engine.ServeHTTP(w, req) @@ -36,11 +45,8 @@ func TestCanvasProxy_StripsAuthorizationHeader(t *testing.T) { } } -// TestCanvasProxy_StripsCookieHeader verifies that session cookies are not -// forwarded to the canvas Next.js server (same rationale as Authorization). func TestCanvasProxy_StripsCookieHeader(t *testing.T) { var capturedCookie string - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedCookie = r.Header.Get("Cookie") w.WriteHeader(http.StatusOK) @@ -51,7 +57,7 @@ func TestCanvasProxy_StripsCookieHeader(t *testing.T) { engine := gin.New() engine.NoRoute(newCanvasProxy(upstream.URL)) - w := httptest.NewRecorder() + w := newTestRecorder() req := httptest.NewRequest("GET", "/canvas-route", nil) req.Header.Set("Cookie", "session=abc123; auth=secret") engine.ServeHTTP(w, req) @@ -61,12 +67,8 @@ func TestCanvasProxy_StripsCookieHeader(t *testing.T) { } } -// TestCanvasProxy_ForwardsOtherHeaders verifies that non-credential headers -// (e.g. Accept, X-Request-ID) still reach the upstream — stripping is -// surgical, not a blanket header wipe. func TestCanvasProxy_ForwardsOtherHeaders(t *testing.T) { var capturedAccept, capturedRequestID string - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedAccept = r.Header.Get("Accept") capturedRequestID = r.Header.Get("X-Request-Id") @@ -78,7 +80,7 @@ func TestCanvasProxy_ForwardsOtherHeaders(t *testing.T) { engine := gin.New() engine.NoRoute(newCanvasProxy(upstream.URL)) - w := httptest.NewRecorder() + w := newTestRecorder() req := httptest.NewRequest("GET", "/page", nil) req.Header.Set("Accept", "text/html") req.Header.Set("X-Request-Id", "trace-abc") @@ -93,11 +95,8 @@ func TestCanvasProxy_ForwardsOtherHeaders(t *testing.T) { } } -// TestCanvasProxy_NoBothCredentialHeaders is the combined regression: a request -// carrying both Authorization AND Cookie must have both stripped. func TestCanvasProxy_NoBothCredentialHeaders(t *testing.T) { var gotAuth, gotCookie string - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") gotCookie = r.Header.Get("Cookie") @@ -109,7 +108,7 @@ func TestCanvasProxy_NoBothCredentialHeaders(t *testing.T) { engine := gin.New() engine.NoRoute(newCanvasProxy(upstream.URL)) - w := httptest.NewRecorder() + w := newTestRecorder() req := httptest.NewRequest("GET", "/dashboard", nil) req.Header.Set("Authorization", "Bearer token123") req.Header.Set("Cookie", "sid=xyz")