Skip to content

Commit

Permalink
Support necessary headers in the web server response (#36122)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
shuding committed Apr 13, 2022
1 parent 74dead2 commit a4a970b
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 6 deletions.
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

0 comments on commit a4a970b

Please sign in to comment.