fix: apply headers, cookies, and status set by getServerSideProps to the response#281
Conversation
…the response getServerSideProps receives a res object that supports setHeader(), statusCode, and end(). Previously, any headers, cookies, or status codes set on this object were silently discarded in the final HTML response. This fixes two code paths: 1. Production (index.ts renderPage): Capture the res object from createReqRes and merge its accumulated headers, cookies, and status into the final Response. Also handle the short-circuit case where gSSP calls res.end() directly. 2. Dev server (dev-server.ts): Preserve res.statusCode set by gSSP so it flows through to streamPageToResponse. Also detect when gSSP calls res.end() to avoid writing to an already-ended stream. Added getHeaders() and headersSent to the mock res object in createReqRes so callers can read accumulated response metadata. Test: Added fixture pages (ssr-headers.tsx, ssr-res-end.tsx) and integration tests verifying custom headers, cookies, status codes, and res.end() short-circuit behavior.
|
/bigbonk review this |
commit: |
|
There was a problem hiding this comment.
Review
Good fix for a real bug — gSSP's res.setHeader(), res.statusCode, and res.end() should absolutely flow through to the actual HTTP response, and this PR addresses that correctly for both dev and prod paths.
What looks good
- Production path (
renderPageinindex.ts): The approach of savinggsspRes, callinggetHeaders()after gSSP returns, and merging into the finalResponseviaHeadersAPI is clean. UsingHeaders.append()for Set-Cookie is correct. res.end()short-circuit: Both paths detect it properly —res.headersSent(mock getter) in prod,res.writableEnded(Node.js native) in dev. TheresponsePromisepattern in prod is a good design for bridging the mock res to a Web Response.getHeaders()andheadersSentadditions tocreateReqRes: These are useful additions that make the mock res more compatible with the Node.js API surface.- Test coverage: Good fixture pages that exercise the key scenarios (custom headers, cookies, status codes, and
res.end()short-circuit). Tests assert the right things. - Content-Type guard: Re-setting
Content-Type: text/htmlafter merging gSSP headers prevents gSSP from accidentally breaking the page render response type.
Issue: Dev server doesn't pass gSSP custom headers
The dev server (dev-server.ts) preserves res.statusCode (lines 463-467), which is great. But it does not pass custom headers set via res.setHeader() (like x-custom-header or set-cookie) through to the streamPageToResponse call.
In the dev path, res is the real Node.js ServerResponse — when gSSP calls res.setHeader("x-custom-header", "hello-from-gssp"), that sets headers on the Node.js response object. But streamPageToResponse then calls res.writeHead(statusCode, headers) (line 143 of dev-server.ts), which replaces any previously set headers rather than merging them.
The dev test passes because streamPageToResponse calls res.writeHead(statusCode, headers) on the same res object that gSSP already called res.setHeader() on, and Node.js writeHead() merges with previously set headers when called with an object argument. So the test works, but it's somewhat accidental — if streamPageToResponse were ever changed to construct a fresh headers object, gSSP headers would be lost.
It would be more robust to explicitly collect gSSP-set headers and pass them through extraHeaders, similar to how the prod path does it. But this is a minor concern since the current behavior works correctly.
Question: prod server parity for res.end() short-circuit
In the prod path (prod-server.ts, lines 916-947), renderPage returns a Response, and that response gets passed through sendCompressed. When gSSP calls res.end() and renderPage returns the responsePromise Response (which has content-type: application/json and status 202), the prod server wraps it through sendCompressed which adds Content-Type from the response. This should work fine.
However, the middleware headers from middlewareHeaders will still be merged into the response even for res.end() short-circuit responses. Is that intentional? In the dev path, middleware headers don't get merged because streamPageToResponse is never called. This is a minor dev/prod inconsistency.
Nit: var vs let/const in generated code
The new code in renderPage (generated server entry) mixes var and const/let:
var gsspRes = null;
// ...
var finalStatus = 200;
const responseHeaders = new Headers({ "Content-Type": "text/html" });The existing generated code in this function uses var throughout (this is ES5-style generated code). The new code introduces const and let (const responseHeaders = new Headers(...), var finalStatus = 200). Not a functional issue since the output targets modern runtimes, but worth noting the inconsistency.
Summary
The fix is correct and well-tested. The one thing worth considering:
- The dev/prod behavior gap for middleware headers on
res.end()short-circuit responses (minor, probably fine for now)
LGTM with those minor notes.
|
I've reviewed and approved PR #281. Here's the summary: Overall: Good fix. The core issue — gSSP headers/cookies/status being silently dropped — is correctly addressed in both the dev server and production server entry paths. Key findings:
|
Address review feedback: - Capture headers newly set by gSSP via snapshot/diff and forward them explicitly through extraHeaders, instead of relying on Node.js writeHead() implicitly merging setHeader() calls. This prevents silent header loss if streamPageToResponse is refactored. - Support array-valued headers (e.g. multiple Set-Cookie) in streamPageToResponse by using res.setHeader() for arrays. - Add comments explaining the middleware header behavior when gSSP calls res.end() (dev includes them via the shared res object, prod merges them via the worker entry).
|
/bigbonk review this one more time |
|
UnknownError: ProviderInitError: ProviderInitError |
|
@southpolesteve Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review this one more time |
|
UnknownError: ProviderInitError: ProviderInitError |
|
/bigbonk review this one more time |
|
I've completed my review. Here's my analysis: Review of PR #281The fix is correct and well-structured. Both the production path ( Bug: Multiple Set-Cookie headers lost in prod-server.ts (pre-existing, but now exposed)
const responseHeaders: Record<string, string> = { ...middlewareHeaders };
response.headers.forEach((v, k) => { responseHeaders[k] = v; });
This PR correctly uses This is a pre-existing bug, not introduced by this PR. But this PR makes it more visible since gSSP can now actually set cookies. Worth a follow-up fix — What looks good
Minor observations
VerdictThe PR is correct for its stated scope. The Set-Cookie flattening issue in |
Summary
getServerSidePropsreceives aresobject that supportssetHeader(),statusCode, andend(). Previously, headers/cookies/status set on this object were silently discarded in the final HTML response.renderPageinindex.ts): the mockresobject's accumulated state is now merged into the finalResponse, including custom headers, Set-Cookie values, and non-200 status codesdev-server.ts):res.statusCodeset by gSSP is now preserved throughstreamPageToResponse, andres.end()short-circuit is detected to avoid writing to an already-ended streamgetHeaders()andheadersSentto the mockresobject increateReqResso callers can read accumulated response metadatares.end()short-circuit behaviorTest plan
Two new fixture pages:
pages/ssr-headers.tsx— gSSP setsx-custom-header, aSet-Cookie, andstatusCode = 201pages/ssr-res-end.tsx— gSSP callsres.end()with a JSON body and status 202Two new integration tests in
pages-router.test.tsverify headers, cookies, status, and short-circuit behavior.