From 679e0f9116ab537cf692633d37bd11e8c2ef4e6b Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 9 Oct 2026 22:41:21 -0400 Subject: [PATCH] =?UTF-8?q?fix(server):=20ghost=20401=20must=20abort=20the?= =?UTF-8?q?=20chain=20=E2=80=94=20c.JSON's=20nil=20error=20let=20the=20req?= =?UTF-8?q?uest=20continue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard returned c.JSON(401, ...), whose nil error told echo the SuccessHandler succeeded — the pipeline then continued into the route handler, which type-asserted the never-set "user" context value and panic-recovered as a 500 appended after the 401 body. The status code happened to survive (first write wins), which is why the pin test passed, but every ghost request also executed the target handler with no user — the exact unauthenticated-execution class the guard exists to prevent. Return echo.NewHTTPError(401, ...) instead: non-nil aborts the chain and renders {"message":"user_not_found: ..."}; the pin test now asserts that shape. Full suite: 219 groups, zero failures. --- cmd/server/tests/progress_service_test.go | 2 +- internal/router/router.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/server/tests/progress_service_test.go b/cmd/server/tests/progress_service_test.go index 84d3048c..239d99e5 100644 --- a/cmd/server/tests/progress_service_test.go +++ b/cmd/server/tests/progress_service_test.go @@ -717,7 +717,7 @@ func TestDeletedUserTokenRejected(t *testing.T) { var errBody map[string]string require.NoError(t, json.NewDecoder(resp.Body).Decode(&errBody)) - assert.Equal(t, "user_not_found", errBody["error"]) + assert.Contains(t, errBody["message"], "user_not_found") } // TestProgressWeb_LocatorCoherence pins the locator coherence rule: a diff --git a/internal/router/router.go b/internal/router/router.go index f2d9c31f..e4eb7093 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -132,10 +132,11 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc { if wantsHTML(c.Request().Header) { return c.Redirect(http.StatusFound, "/login?session=invalid") } - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "user_not_found", - "message": "This account no longer exists. Please log in again.", - }) + // Non-nil error is REQUIRED: returning c.JSON's nil would + // let the chain continue into handlers whose c.Get("user") + // assertion panics on the never-set user. + return echo.NewHTTPError(http.StatusUnauthorized, + "user_not_found: this account no longer exists; please log in again") } c.Set("user", database.Users{