diff --git a/packages/next/server/api-utils/web.ts b/packages/next/server/api-utils/web.ts new file mode 100644 index 000000000000000..77155aa29bea169 --- /dev/null +++ b/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 + '"' +} diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 4182c953f1663b3..b83440b2ca16661 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -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) { diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index dab158fffb9d7e1..7924aaa4121fb75 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -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 @@ -149,6 +150,8 @@ export default class NextWebServer extends BaseServer { options?: PayloadOptions | undefined } ): Promise { + 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) } diff --git a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js index c3a2c276769df6b..eb7462329cfe983 100644 --- a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js +++ b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js @@ -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)', () => {