Support necessary headers in the web server response (#36122)
This PR adds support of `Content-Length`, `Etag` and `X-Edge-Runtime` headers to the web server. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
This commit is contained in:
parent
74dead2489
commit
a4a970bafa
4 changed files with 52 additions and 6 deletions
28
packages/next/server/api-utils/web.ts
Normal file
28
packages/next/server/api-utils/web.ts
Normal file
|
@ -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 + '"'
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
@ -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') {
|
||||
|
@ -163,12 +166,11 @@ 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: () => {},
|
||||
|
@ -176,8 +178,11 @@ export default class NextWebServer extends BaseServer {
|
|||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
launchApp,
|
||||
nextBuild,
|
||||
nextStart,
|
||||
fetchViaHTTP,
|
||||
renderViaHTTP,
|
||||
waitFor,
|
||||
} from 'next-test-utils'
|
||||
|
@ -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)', () => {
|
||||
|
|
Loading…
Reference in a new issue