mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Fix(pages-router): restore Content-Length and ETag for /_next/data/ JSON responses (#90304)
## What Removes the `Buffer.from()` wrapper when constructing `RenderResult` for `/_next/data/` JSON responses in the Pages Router handler. ## Why PR #80189 introduced `Buffer.from(JSON.stringify(result.value.pageData))` when building the data response. Since `RenderResult.isDynamic` checks `typeof this.response !== 'string'`, passing a `Buffer` (not a `string`) caused it to return `true`, making `sendRenderResult` treat the response as a dynamic stream — skipping `Content-Length` and `ETag` generation and falling back to `Transfer-Encoding: chunked`. This is a regression from v15.4.0 and breaks CDN-side compression for self-hosted deployments (e.g. CloudFront requires `Content-Length` to compress origin responses on-the-fly). ## Fix ```diff - Buffer.from(JSON.stringify(result.value.pageData)), + JSON.stringify(result.value.pageData), ``` ## Testing - Reproduction steps verified against the reporter's repro repo: https://github.com/bbrouse/nextjs-content-length-repro - ```diffcurl -sD - on /_next/data/<BUILD_ID>/index.json``` now returns Content-Length and ETag headers. ## Affected Area - Pages Router — /_next/data/ responses only - No impact on App Router - Single-line change, minimal blast radius Fixes #90281 --------- Co-authored-by: JJ Kasper <[email protected]>
This commit is contained in:
@@ -148,13 +148,13 @@ pnpm test-dev-turbo test/development/
|
||||
Generating tests using `pnpm new-test` is mandatory.
|
||||
|
||||
```bash
|
||||
# Use --args for non-interactive mode
|
||||
# Format: pnpm new-test --args <appDir> <name> <type>
|
||||
# Use --args for non-interactive mode (forward args to the script using `--`)
|
||||
# Format: pnpm new-test -- --args <appDir> <name> <type>
|
||||
# appDir: true/false (is this for app directory?)
|
||||
# name: test name (e.g. "my-feature")
|
||||
# type: e2e | production | development | unit
|
||||
|
||||
pnpm new-test --args true my-feature e2e
|
||||
pnpm new-test -- --args true my-feature e2e
|
||||
```
|
||||
|
||||
**Analyzing test output efficiently:**
|
||||
@@ -400,7 +400,7 @@ Core runtime/bundling rules (always apply; skills above expand on these with ver
|
||||
### Test Gotchas
|
||||
|
||||
- **Cache components enables PPR by default**: When `__NEXT_CACHE_COMPONENTS=true`, most app-dir pages use PPR implicitly. Dedicated `ppr-full/` and `ppr/` test suites are mostly `describe.skip` (migrating to cache components). To test PPR codepaths, run normal app-dir e2e tests with `__NEXT_CACHE_COMPONENTS=true` rather than looking for explicit PPR test suites.
|
||||
- **Quick smoke testing with toy apps**: For fast feedback, generate a minimal test fixture with `pnpm new-test --args true <name> e2e`, then run the dev server directly with `node packages/next/dist/bin/next dev --port <port>` and `curl --max-time 10`. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes.
|
||||
-- **Quick smoke testing with toy apps**: For fast feedback, generate a minimal test fixture with `pnpm new-test -- --args true <name> e2e`, then run the dev server directly with `node packages/next/dist/bin/next dev --port <port>` and `curl --max-time 10`. This avoids the overhead of the full test harness and gives immediate feedback on hangs/crashes.
|
||||
- Mode-specific tests need `skipStart: true` + manual `next.start()` in `beforeAll` after mode check
|
||||
- Don't rely on exact log messages - filter by content patterns, find sequences not positions
|
||||
- **Snapshot tests vary by env flags**: Tests with inline snapshots can produce different output depending on env flags. When updating snapshots, always run the test with the exact env flags the CI job uses (check `.github/workflows/build_and_test.yml` `afterBuild:` sections). Turbopack resolves `react-dom/server.edge` (no Node APIs like `renderToPipeableStream`), while webpack resolves the `.node` build (has them).
|
||||
|
||||
@@ -526,16 +526,13 @@ export const getHandler = ({
|
||||
return {
|
||||
value: {
|
||||
kind: CachedRouteKind.PAGES,
|
||||
html: new RenderResult(
|
||||
Buffer.from(previousCacheEntry.value.html),
|
||||
{
|
||||
contentType: HTML_CONTENT_TYPE_HEADER,
|
||||
metadata: {
|
||||
statusCode: previousCacheEntry.value.status,
|
||||
headers: previousCacheEntry.value.headers,
|
||||
},
|
||||
}
|
||||
),
|
||||
html: new RenderResult(previousCacheEntry.value.html, {
|
||||
contentType: HTML_CONTENT_TYPE_HEADER,
|
||||
metadata: {
|
||||
statusCode: previousCacheEntry.value.status,
|
||||
headers: previousCacheEntry.value.headers,
|
||||
},
|
||||
}),
|
||||
pageData: {},
|
||||
status: previousCacheEntry.value.status,
|
||||
headers: previousCacheEntry.value.headers,
|
||||
@@ -740,13 +737,10 @@ export const getHandler = ({
|
||||
// anymore
|
||||
result:
|
||||
isNextDataRequest && !isErrorPage && !is500Page
|
||||
? new RenderResult(
|
||||
Buffer.from(JSON.stringify(result.value.pageData)),
|
||||
{
|
||||
contentType: JSON_CONTENT_TYPE_HEADER,
|
||||
metadata: result.value.html.metadata,
|
||||
}
|
||||
)
|
||||
? new RenderResult(JSON.stringify(result.value.pageData), {
|
||||
contentType: JSON_CONTENT_TYPE_HEADER,
|
||||
metadata: result.value.html.metadata,
|
||||
})
|
||||
: result.value.html,
|
||||
generateEtags: nextConfig.generateEtags,
|
||||
poweredByHeader: nextConfig.poweredByHeader,
|
||||
|
||||
@@ -2063,7 +2063,10 @@ describe('Prerender', () => {
|
||||
|
||||
it('should handle revalidating JSON correctly', async () => {
|
||||
const route = `/_next/data/${next.buildId}/blog/post-2/comment-3.json`
|
||||
const initialJson = await renderViaHTTP(next.url, route)
|
||||
const initialRes = await fetchViaHTTP(next.url, route)
|
||||
const initialJson = await initialRes.text()
|
||||
expect(initialRes.headers.get('Content-Length')).toBeDefined()
|
||||
expect(initialRes.headers.get('ETag')).toBeDefined()
|
||||
expect(initialJson).toMatch(/post-2/)
|
||||
expect(initialJson).toMatch(/comment-3/)
|
||||
|
||||
@@ -2078,7 +2081,10 @@ describe('Prerender', () => {
|
||||
await renderViaHTTP(next.url, route)
|
||||
|
||||
await check(async () => {
|
||||
newJson = await renderViaHTTP(next.url, route)
|
||||
const newRes = await fetchViaHTTP(next.url, route)
|
||||
expect(newRes.headers.get('Content-Length')).toBeDefined()
|
||||
expect(newRes.headers.get('ETag')).toBeDefined()
|
||||
newJson = await newRes.text()
|
||||
return newJson !== initialJson ? 'success' : newJson
|
||||
}, 'success')
|
||||
|
||||
@@ -2135,7 +2141,10 @@ describe('Prerender', () => {
|
||||
|
||||
it('should handle revalidating HTML correctly with blocking and seed', async () => {
|
||||
const route = '/blocking-fallback/a'
|
||||
const initialHtml = await renderViaHTTP(next.url, route)
|
||||
const initialRes = await fetchViaHTTP(next.url, route)
|
||||
const initialHtml = await initialRes.text()
|
||||
expect(initialRes.headers.get('Content-Length')).toBeDefined()
|
||||
expect(initialRes.headers.get('ETag')).toBeDefined()
|
||||
const $initial = cheerio.load(initialHtml)
|
||||
expect($initial('p').text()).toBe('Post: a')
|
||||
|
||||
@@ -2150,7 +2159,10 @@ describe('Prerender', () => {
|
||||
await renderViaHTTP(next.url, route)
|
||||
|
||||
await check(async () => {
|
||||
newHtml = await renderViaHTTP(next.url, route)
|
||||
const newRes = await fetchViaHTTP(next.url, route)
|
||||
expect(newRes.headers.get('Content-Length')).toBeDefined()
|
||||
expect(newRes.headers.get('ETag')).toBeDefined()
|
||||
newHtml = await newRes.text()
|
||||
return newHtml !== initialHtml ? 'success' : newHtml
|
||||
}, 'success')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user