Skip to content

fix(graphql-server): surface CSRF rejections as JSON - #1773

Merged
pyramation merged 1 commit into
mainfrom
fix/csrf-error-surfacing
Aug 22, 2026
Merged

fix(graphql-server): surface CSRF rejections as JSON#1773
pyramation merged 1 commit into
mainfrom
fix/csrf-error-surfacing

Conversation

@pyramation

@pyramation pyramation commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

GraphQL JSON requests without an explicit Accept header were misclassified as HTML clients by wantsJson. CSRF middleware correctly raised a typed error and the centralized handler preserved status 403, but sendResponse rendered every non-5xx HTML error with the Not Found page.

This changes content negotiation to also recognize a JSON request Content-Type, preserving the existing structured CSRF response for API clients while leaving genuine browser/HTML 404s unchanged.

Closes #1681.

Before / after transcript

Requests below omit Accept, matching the reported flow. Tokens and request IDs are redacted.

Scenario Before on main After
session + CSRF cookie + matching X-CSRF-Token 200 application/graphql-response+json {"data":{"__typename":"Query"}} unchanged
missing X-CSRF-Token 403 text/html Not Found page 403 application/json {"error":{"code":"CSRF_TOKEN_INVALID","message":"CSRF token validation failed","requestId":"…"}}
session cookie only / missing CSRF cookie 403 text/html Not Found page 403 application/json {"error":{"code":"CSRF_TOKEN_MISSING","message":"CSRF token missing from cookie","requestId":"…"}}
wrong X-CSRF-Token 403 text/html Not Found page 403 application/json {"error":{"code":"CSRF_TOKEN_INVALID","message":"CSRF token validation failed","requestId":"…"}}
unknown route 404 text/html <title>Not Found</title> / The requested page was not found unchanged

The JSON error never includes the expected token value.

Edge transcript:

Case Status Content-Type ACAO Body
GET /graphql token issuance 405 application/json absent {"errors":[{"message":"Method not supported, please use POST","extensions":{}}]}; sets csrf_token=<redacted>
wrong Origin + session cookie only 403 application/json absent CSRF_TOKEN_MISSING after the fix; previously the same 403 rendered the Not Found HTML page
wrong Origin + valid CSRF cookie/header 200 application/graphql-response+json absent {"data":{"__typename":"Query"}} (unchanged)

Root cause and regression proof

The issue was not CSRF middleware ordering or 404 fallthrough: the error reached errorHandler, was categorized as 403, then the Accept-only wantsJson check selected the HTML branch and its generic 4xx renderer. Recognizing the JSON request content type fixes the representation at that content-negotiation boundary.

The new five-case E2E block uses the existing @constructive-io/graphql-server-test HTTP harness. Against unmodified production code on main, the happy flow and genuine 404 passed, while all three CSRF rejection tests failed specifically because they received text/html instead of JSON (3 failed, 2 passed). With this change, all five pass.

Validation:

  • graphql/server: 13 suites, 143 tests passed
  • graphql/server-test/server.integration.test.ts: 39 tests passed
  • graphql/server-test/scoped-routing.integration.test.ts: 7 tests passed
  • affected-package lint: no errors
  • affected-package builds: passed
  • PR CI: 20 passed, 0 failed, 1 skipped

Deliberately left alone

  • The local fixtures have no auth module, so the E2E synthesizes the session cookie; csrfProtect only checks cookie presence, so it exercises the same CSRF branch, but a real sign-in/Set-Cookie leg was not locally available.
  • The CSRF cookie currently emits Max-Age=86 because of a pre-existing unit mismatch.
  • A wrong-Origin request with a valid double-submit pair succeeds server-side but receives no Access-Control-Allow-Origin; adding server-side Origin validation is separate from this error-surfacing fix.
  • GET /graphql returns 405 while setting the token cookie, and CSRF 403s are logged as unexpected_error; both are pre-existing and outside this focused fix.

Link to Devin session: https://app.devin.ai/sessions/311a44c399bc4b10b4eafa163d9660ba
Requested by: @pyramation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review complete. No issues found — approved ✅.


This PR makes two focused changes. It adds an integration test suite covering the cookie-authenticated GraphQL CSRF flow (valid token, missing token, missing cookie, wrong token, and HTML 404 for unknown routes), and it extends the error handler's wantsJson() detection with a Content-Type check so JSON requests receive JSON error responses. The changes are small and well-scoped.

Files Change
graphql/server/src/middleware/error-handler.ts Extends wantsJson() to also return true when the request Content-Type is JSON.
graphql/server-test/tests/server.integration.test.ts Adds a cookie-authenticated GraphQL CSRF flow test suite covering token/cookie handling and HTML 404 routing.

Reviewed commit: 4db3124

@pyramation
pyramation merged commit f288963 into main Aug 22, 2026
21 checks passed
@pyramation
pyramation deleted the fix/csrf-error-surfacing branch August 22, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

403 CSRF error displays misleading 'Not Found' page

1 participant