Skip to content

Commit

Permalink
Forward headers from React to static output and dynamic render (verce…
Browse files Browse the repository at this point in the history
…l#58162)

React can emit a `Link:` header for preloads instead of `<link
rel="preload">` in certain scenarios when that can be useful. This works
by listening to the `onHeaders` event.

In particular it's interesting for PPR because if you have something
dynamic outside a Suspense boundary it generates an empty payload
without any preloads in it. That's because when we do render the real
shell we don't know what the document will look like. However, we can
emit the `Link` header for CSS, images and font preloads that we've
already discovered. In effect, even a dynamic page gets PPR benefits by
early fetching resources.

Custom headers is supported for static a ROUTE but not a PAGE. So I had
to add similar wiring to forward headers when it's a page being
rendered.

It's important that this works every where, including dynamic routes,
because otherwise we might miss out on preloads that we previously
would've had.
  • Loading branch information
sebmarkbage authored Nov 8, 2023
1 parent 24baf8f commit 1063021
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 7 deletions.
6 changes: 3 additions & 3 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ExportRouteResult, FileWriter } from '../types'
import type { RenderOpts } from '../../server/app-render/types'
import type { OutgoingHttpHeaders } from 'http'
import type { NextParsedUrlQuery } from '../../server/request-meta'
import type { RouteMetadata } from './types'

Expand Down Expand Up @@ -184,9 +183,10 @@ export async function exportAppPage(
)
}

let headers: OutgoingHttpHeaders | undefined
const headers = { ...metadata.extraHeaders }

if (fetchTags) {
headers = { [NEXT_CACHE_TAGS_HEADER]: fetchTags }
headers[NEXT_CACHE_TAGS_HEADER] = fetchTags
}

// Writing static HTML to a file.
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -736,9 +736,21 @@ async function renderToHTMLOrFlightImpl(
hasPostponed,
})

function onHeaders(headers: Headers): void {
// Copy headers created by React into the response object.
headers.forEach((value: string, key: string) => {
res.appendHeader(key, value)
if (!extraRenderResultMeta.extraHeaders) {
extraRenderResultMeta.extraHeaders = {}
}
extraRenderResultMeta.extraHeaders[key] = value
})
}

try {
const renderStream = await renderer.render(content, {
onError: htmlRendererErrorHandler,
onHeaders: onHeaders,
nonce,
bootstrapScripts: [bootstrapScript],
formState,
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/server/app-render/static/static-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type StreamOptions = {
onError?: (error: Error) => void
onHeaders?: (headers: Headers) => void
nonce?: string
bootstrapScripts?: {
src: string
Expand Down Expand Up @@ -38,6 +39,10 @@ class StaticResumeRenderer implements Renderer {
constructor(private readonly postponed: object) {}

public async render(children: JSX.Element, streamOptions: StreamOptions) {
// TODO: Refactor StreamOptions because not all options apply to all React
// functions so this factoring of trying to reuse a single render() doesn't
// make sense. This is passing multiple invalid options that React should
// error for.
const stream = await this.resume(children, this.postponed, streamOptions)

return { stream }
Expand Down
24 changes: 21 additions & 3 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2373,10 +2373,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {

// Add any fetch tags that were on the page to the response headers.
const cacheTags = metadata.fetchTags

headers = { ...metadata.extraHeaders }

if (cacheTags) {
headers = {
[NEXT_CACHE_TAGS_HEADER]: cacheTags,
}
headers[NEXT_CACHE_TAGS_HEADER] = cacheTags
}

// Pull any fetch metrics from the render onto the request.
Expand Down Expand Up @@ -2769,6 +2770,23 @@ export default abstract class Server<ServerOptions extends Options = Options> {
)
}

if (cachedData.headers) {
const resHeaders = cachedData.headers
for (const key of Object.keys(resHeaders)) {
if (key === NEXT_CACHE_TAGS_HEADER) {
// Not sure if needs to be special.
continue
}
let v = resHeaders[key]
if (typeof v !== 'undefined') {
if (typeof v === 'number') {
v = v.toString()
}
res.setHeader(key, v)
}
}
}

if (
this.minimalMode &&
isSSG &&
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/render-result.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServerResponse } from 'http'
import type { OutgoingHttpHeaders, ServerResponse } from 'http'
import type { StaticGenerationStore } from '../client/components/static-generation-async-storage.external'
import type { Revalidate } from './lib/revalidate'

Expand All @@ -23,6 +23,7 @@ export type RenderResultMetadata = {
isRedirect?: boolean
fetchMetrics?: StaticGenerationStore['fetchMetrics']
fetchTags?: string
extraHeaders?: OutgoingHttpHeaders
waitUntil?: Promise<any>
postponed?: string
}
Expand Down

0 comments on commit 1063021

Please sign in to comment.