cloudflare / cloudflare/vinext
Pages Router: _next/data responses drop gSSP status and fold multiple Set-Cookie values (prod only)
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
Production `/_next/data//.json` responses drop the status code set by `getServerSideProps` and fold multiple `Set-Cookie` values into a single header. The HTML response for the same page does neither, and the dev server does neither.
The related `gsspRes.headers` bug — the data path reading a property that does not exist, so `Cache-Control`, `Vary`, and `Set-Cookie` were dropped entirely — was fixed in 0e6414717 (#1573). What remains is narrower, and is a parity defect rather than a cache-safety one: since that commit, a data response with a `gsspRes` and no app-set `Cache-Control` defaults to `private, no-cache, no-store, max-age=0, must-revalidate`, and app-set `Cache-Control`/`Vary` are forwarded.
## Possible root cause
`packages/vinext/src/server/pages-page-handler.ts:937-992` translates the mutable Pages response shim (`PagesGsspResponse`, declared at `packages/vinext/src/server/pages-page-response.ts:105-112`, implemented at `packages/vinext/src/server/pages-node-compat.ts:356`) into a `fetch` `Response` for data requests. It open-codes that translation as a flat `Record` instead of calling `applyGsspHeaders` (`packages/vinext/src/server/pages-page-response.ts:464-493`), which already encodes the two rules the copy gets wrong.
**Status.** `pages-page-handler.ts:938` builds `init` with `headers` only. `init.status` is never set from `gsspRes.statusCode`, and `buildNextDataPropsJsonResponse` defaults to `status: init?.status ?? 200` (`packages/vinext/src/server/pages-data-route.ts:141-154`). The HTML path returns `statusCode ?? gsspRes.statusCode` (`pages-page-response.ts:492`); dev picks the status up at `packages/vinext/src/server/dev-server.ts:1272-1275` and writes it at `dev-server.ts:1502`.
**Cookies.** `pages-page-handler.ts:944` does `init.headers[k] = Array.isArray(v) ? v.join(", ") : String(v)` with no `set-cookie` case. The HTML path appends each cookie separately (`pages-page-response.ts:477-482`). One cookie survives; two or more join into a single header, and `Expires=Wed, 21 Oct ...` values contain commas, so the result is unparseable.
Two independent translations of one shim drifted apart. The `gsspRes.headers` typo was the same drift in a more severe form, which is why fixing it left these two standing.
## What reaches it
Production Pages Router only. No non-default config: any page with `getServerSideProps` is served at `/_next/data//.json`, the buildId is in `__NEXT_DATA__` in every HTML response, and the client router fetches that URL on each client-side navigation. So a page that behaves correctly on a full page load misbehaves after a client-side `` navigation to the same route — a gSSP-set 401 or 404 arrives as 200, and a page setting two or more cookies in `getServerSideProps` loses them.
The App Router bridge (`packages/vinext/src/server/app-pages-bridge.ts:176`) delegates to the same `renderPage`, so it inherits this rather than having its own copy.
## Repro / failing-test sketch
Not executed — a sketch against the boundary `tests/pages-page-handler.test.ts` already uses. That file calls the handler with a data request at line 829:
```js
const res = await handler(makeRequest("/about"), "/about", null, null, { isDataReq: true });
```
Add a page whose `getServerSideProps` does:
```js
res.statusCode = 401;
res.setHeader("Set-Cookie", ["a=1; Path=/", "b=2; Path=/"]);
return { props: {} };
```
Expected: `res.status === 401` and `res.headers.getSetCookie().length === 2`.
Actual: `200`, and one folded header `a=1; Path=/, b=2; Path=/`.
The same request without `{ isDataReq: true }` passes both assertions. That gap is the bug.
## Possible fix direction
Have `pages-page-handler.ts:937-992` reuse the existing translator: build a `Headers`, call `applyGsspHeaders(headers, gsspRes)` for the per-cookie append and the effective status, set `Content-Type: application/json`, then layer the existing default-`Cache-Control`, ISR, and deployment-id logic on top and pass both headers and status to `buildNextDataPropsJsonResponse`.
Two things to watch. `applyGsspHeaders` hard-codes `text/html; charset=utf-8` at `pages-page-response.ts:491`, so it needs a content-type parameter or a content-type-agnostic variant, and the `hasUserCacheControl` loop at `pages-page-handler.ts:951-956` iterates plain object keys, so it becomes `headers.has("cache-control")`. Forwarding a non-200 status also changes behavior for anything branching on data responses being 200: dev sets `x-nextjs-matched-path` only at status 200 (`dev-server.ts:1479`), and anything treating a 404 data response as "page missing" is worth re-reading before the status change lands.
## Verified how
Everything above comes from reading the tree at `c9a4a843c` and from `git log`. I ran no tests and made no changes; the repro is a sketch, not a run.
Unverified: whether upstream Next.js preserves a gSSP-set status on `_next/data` responses. I spot-checked `isNextDataRequest` in `packages/next/src/server/render.tsx` but did not trace upstream's status plumbing, so treat the Next.js parity question as open. vinext's own dev/prod and HTML/data divergence is the argument here regardless of what upstream does.
Contributor guide
Research direction
Start with packages/vinext/src/server/pages-page-handler.ts:937-992 and compare its data-response translation with applyGsspHeaders in packages/vinext/src/server/pages-page-response.ts:464-493. Run or extend the boundary cases in tests/pages-page-handler.test.ts around line 829, covering a gSSP status and multiple cookies for data and HTML requests. Done means production data responses preserve the status and separate Set-Cookie values without regressing cache, ISR, or deployment headers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100