Skip to content

Commit

Permalink
[ppr] Configuration for max react headers length (#67715)
Browse files Browse the repository at this point in the history
### What?

This exposes a new `reactMaxHeadersLength` config option for
`next.config.js`:

```js
module.exports = {
  reactMaxHeadersLength: 1000,
}
```

That allows configuration of the maximum value of all the headers
generated from React. Currently only added when the experimental partial
prerendering (PPR) feature is enabled, this today only affects the size
of the `Link` header that is emitted which contains preloads for assets
like fonts.

This also bumps the default value from 600 to 6000.

### Why?

Some proxies may have differing support for max header sizes, so this
was added as a configuration option to allow users to better control the
output experience in these cases. At the time of writing, most proxies
support a maximum of 8k bytes in the header, which is above the default
6k, allowing for other headers written by Next.js.

---------

Co-authored-by: Sam Ko <[email protected]>
  • Loading branch information
wyattjoh and samcx authored Aug 2, 2024
1 parent c4dd908 commit 85383a6
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: reactMaxHeadersLength
description: The maximum length of the headers that are emitted by React and added to the response.
---

During static rendering, React can emit headers that can be added to the response. These can be used to improve performance by allowing the browser to preload resources like fonts, scripts, and stylesheets. The default value is `6000`, but you can override this value by configuring the `reactMaxHeadersLength` option in `next.config.js`:

```js filename="next.config.js"
module.exports = {
reactMaxHeadersLength: 1000,
}
```

> **Good to know**: This option is only available in App Router.
Depending on the type of proxy between the browser and the server, the headers can be truncated. For example, if you are using a reverse proxy that doesn't support long headers, you should set a lower value to ensure that the headers are not truncated.
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export async function exportAppImpl(
swrDelta: nextConfig.swrDelta,
after: nextConfig.experimental.after ?? false,
},
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1106,7 +1106,7 @@ async function renderToHTMLOrFlightImpl(
streamOptions: {
onError: htmlRendererErrorHandler,
onHeaders,
maxHeadersLength: 600,
maxHeadersLength: renderOpts.reactMaxHeadersLength,
nonce,
// When debugging the static shell, client-side rendering should be
// disabled to prevent blanking out the page.
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ export interface RenderOptsPartial {
* statically generated.
*/
isDebugDynamicAccesses?: boolean

/**
* The maximum length of the headers that are emitted by React and added to
* the response.
*/
reactMaxHeadersLength: number | undefined

isStaticGeneration?: boolean
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export default abstract class Server<
},
onInstrumentationRequestError:
this.instrumentationOnRequestError.bind(this),
reactMaxHeadersLength: this.nextConfig.reactMaxHeadersLength,
}

// Initialize next/config with the environment configuration
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
publicRuntimeConfig: z.record(z.string(), z.any()).optional(),
reactProductionProfiling: z.boolean().optional(),
reactStrictMode: z.boolean().nullable().optional(),
reactMaxHeadersLength: z.number().nonnegative().int().optional(),
redirects: z
.function()
.args()
Expand Down
9 changes: 9 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,14 @@ export interface NextConfig extends Record<string, any> {
*/
reactStrictMode?: boolean | null

/**
* The maximum length of the headers that are emitted by React and added to
* the response.
*
* @see [React Max Headers Length](https://nextjs.org/docs/api-reference/next.config.js/react-max-headers-length)
*/
reactMaxHeadersLength?: number

/**
* Add public (in browser) runtime configuration to your app
*
Expand Down Expand Up @@ -936,6 +944,7 @@ export const defaultConfig: NextConfig = {
publicRuntimeConfig: {},
reactProductionProfiling: false,
reactStrictMode: null,
reactMaxHeadersLength: 6000,
httpAgentOptions: {
keepAlive: true,
},
Expand Down
23 changes: 23 additions & 0 deletions test/e2e/app-dir/react-max-headers-length/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactNode } from 'react'
import { preload } from 'react-dom'

export default function Root({ children }: { children: ReactNode }) {
// Each of these preloads will emit a link header that will consist of about
// 105 characters.
for (let i = 0; i < 100; i++) {
preload(
'/?q=some+string+that+spans+lots+of+characters&i=' +
String(i).padStart(2, '0'),
{
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
}
)
}
return (
<html>
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/react-max-headers-length/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
14 changes: 14 additions & 0 deletions test/e2e/app-dir/react-max-headers-length/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
reactMaxHeadersLength: process.env.TEST_REACT_MAX_HEADERS_LENGTH
? parseInt(process.env.TEST_REACT_MAX_HEADERS_LENGTH)
: undefined,
experimental: {
// Emitting Link headers currently requires the experimental PPR feature.
ppr: true,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { nextTestSetup } from 'e2e-utils'

const LINK_HEADER_SIZE = 111

/**
* Calculates the minimum header length that will be emitted by the server. This
* is calculated by taking the maximum header length and dividing it by the
* average header size including joining `, ` characters.
*
* @param maxLength the maximum header length
* @returns the minimum header length
*/
function calculateMinHeaderLength(maxLength) {
const averageHeaderSize = LINK_HEADER_SIZE + 2
return Math.floor(maxLength / averageHeaderSize) * averageHeaderSize - 2
}

describe('react-max-headers-length', () => {
describe.each([0, 400, undefined, 10000])(
'reactMaxHeadersLength = %s',
(reactMaxHeadersLength) => {
const env: Record<string, string> = {}
if (typeof reactMaxHeadersLength === 'number') {
env.TEST_REACT_MAX_HEADERS_LENGTH = reactMaxHeadersLength.toString()
}

const { next } = nextTestSetup({ files: __dirname, env })

it('should respect reactMaxHeadersLength', async () => {
const res = await next.fetch('/')

// React currently only sets the `Link` header, so we should check to
// see that the length of the header has respected the configured
// value.
const header = res.headers.get('Link')
if (reactMaxHeadersLength === undefined) {
// This is the default case.
expect(header).toBeString()

expect(header.length).toBeGreaterThanOrEqual(
calculateMinHeaderLength(6000)
)
expect(header.length).toBeLessThanOrEqual(6000)
} else if (reactMaxHeadersLength === 0) {
// This is the case where the header is not emitted.
expect(header).toBeNull()
} else if (typeof reactMaxHeadersLength === 'number') {
// This is the case where the header is emitted and the length is
// respected.
expect(header).toBeString()

expect(header.length).toBeGreaterThanOrEqual(
calculateMinHeaderLength(reactMaxHeadersLength)
)
expect(header.length).toBeLessThanOrEqual(reactMaxHeadersLength)
}
})
}
)
})
3 changes: 2 additions & 1 deletion test/ppr-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"test/e2e/app-dir/searchparams-static-bailout/searchparams-static-bailout.test.ts",
"test/e2e/app-dir/app-client-cache/client-cache.experimental.test.ts",
"test/e2e/app-dir/app-client-cache/client-cache.original.test.ts",
"test/e2e/app-dir/app-client-cache/client-cache.defaults.test.ts"
"test/e2e/app-dir/app-client-cache/client-cache.defaults.test.ts",
"test/e2e/app-dir/react-max-headers-length/react-max-headers-length.test.ts"
]
}
}

0 comments on commit 85383a6

Please sign in to comment.