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:
Shu Ding 2022-04-13 19:14:53 +02:00 committed by GitHub
parent 74dead2489
commit a4a970bafa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 6 deletions

View 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 + '"'
}

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)', () => {