cloudflare / cloudflare/vinext

Pages Router: _next/data responses drop gSSP status and fold multiple Set-Cookie values (prod only)

Open
#2,779 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.