Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support necessary headers in the web server response #36122

Merged
merged 6 commits into from Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/next/server/api-utils/web.ts
@@ -0,0 +1,28 @@
// Buffer.byteLength polyfill in the Edge runtime, with only utf8 strings
// supported at the moment.
export function byteLength(payload: string): number {
return new TextEncoder().encode(payload).buffer.byteLength
}

// Calculate the ETag for a payload.
export async function generateETag(payload: string) {
if (payload.length === 0) {
// fast-path empty
return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
}

// compute hash of entity
const hash = btoa(
String.fromCharCode.apply(
null,
new Uint8Array(
await crypto.subtle.digest('SHA-1', new TextEncoder().encode(payload))
) as any
)
).substring(0, 27)

// compute length of entity
const len = byteLength(payload)

return '"' + len.toString(16) + '-' + hash + '"'
}
4 changes: 3 additions & 1 deletion packages/next/server/render.tsx
Expand Up @@ -1749,7 +1749,9 @@ export async function renderToHTML(
return new RenderResult(html)
}

return new RenderResult(chainStreams(streams))
return new RenderResult(
chainStreams(streams).pipeThrough(createBufferedTransformStream())
)
}

function errorToJSON(err: Error) {
Expand Down
15 changes: 10 additions & 5 deletions packages/next/server/web-server.ts
Expand Up @@ -8,6 +8,7 @@ import type { LoadComponentsReturnType } from './load-components'

import BaseServer, { Options } from './base-server'
import { renderToHTML } from './render'
import { byteLength, generateETag } from './api-utils/web'

interface WebServerConfig {
loadComponent: (pathname: string) => Promise<LoadComponentsReturnType | null>
Expand Down Expand Up @@ -149,6 +150,8 @@ export default class NextWebServer extends BaseServer {
options?: PayloadOptions | undefined
}
): Promise<void> {
res.setHeader('X-Edge-Runtime', '1')

// Add necessary headers.
// @TODO: Share the isomorphic logic with server/send-payload.ts.
if (options.poweredByHeader && options.type === 'html') {
Expand All @@ -163,21 +166,23 @@ export default class NextWebServer extends BaseServer {
)
}

// @TODO
const writer = res.transformStream.writable.getWriter()

if (options.result.isDynamic()) {
const writer = res.transformStream.writable.getWriter()
options.result.pipe({
write: (chunk: Uint8Array) => writer.write(chunk),
write: (chunk: Uint8Array) =>
writer.write(new TextDecoder().decode(chunk)),
end: () => writer.close(),
destroy: (err: Error) => writer.abort(err),
cork: () => {},
uncork: () => {},
// Not implemented: on/removeListener
} as any)
} else {
// TODO: generate Etag
const payload = await options.result.toUnchunkedString()
res.setHeader('Content-Length', String(byteLength(payload)))
if (options.generateEtags) {
res.setHeader('ETag', await generateETag(payload))
}
res.body(payload)
}

Expand Down
Expand Up @@ -8,6 +8,7 @@ import {
launchApp,
nextBuild,
nextStart,
fetchViaHTTP,
renderViaHTTP,
waitFor,
} from 'next-test-utils'
Expand Down Expand Up @@ -250,6 +251,16 @@ describe('Switchable runtime (prod)', () => {
'This is a static RSC page.'
)
})

it('should support etag header in the web server', async () => {
const res = await fetchViaHTTP(context.appPort, '/edge', '', {
headers: {
// Make sure the result is static so an etag can be generated.
'User-Agent': 'Googlebot',
},
})
expect(res.headers.get('ETag')).toBeDefined()
})
})

describe('Switchable runtime (dev)', () => {
Expand Down