From d41ca43d23614be45191b8095edddb72d255e6a3 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sun, 18 Sep 2022 22:49:05 +0200 Subject: [PATCH] Change Flight response content type to application/octet-stream (#40665) Ensures Flight responses are not loaded as HTML. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a 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 a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/next/server/app-render.tsx | 13 +++++++++++-- packages/next/server/render-result.ts | 15 +++++++++++++-- packages/next/server/send-payload/index.ts | 8 +++++++- packages/next/server/web-server.ts | 6 +++++- test/e2e/app-dir/index.test.ts | 13 +++++++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 766d84d0e426..ae12b0335b5b 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -51,6 +51,15 @@ export type RenderOptsPartial = { export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial +/** + * Flight Response is always set to application/octet-stream to ensure it does not + */ +class FlightRenderResult extends RenderResult { + constructor(response: string | ReadableStream) { + super(response, { contentType: 'application/octet-stream' }) + } +} + /** * Interop between "export default" and "module.exports". */ @@ -500,7 +509,7 @@ export async function renderToHTMLOrFlight( // Empty so that the client-side router will do a full page navigation. const flightData: FlightData = pathname + (search ? `?${search}` : '') - return new RenderResult( + return new FlightRenderResult( renderToReadableStream(flightData, serverComponentManifest).pipeThrough( createBufferedTransformStream() ) @@ -1054,7 +1063,7 @@ export async function renderToHTMLOrFlight( ).slice(1), ] - return new RenderResult( + return new FlightRenderResult( renderToReadableStream(flightData, serverComponentManifest, { context: serverContexts, }).pipeThrough(createBufferedTransformStream()) diff --git a/packages/next/server/render-result.ts b/packages/next/server/render-result.ts index e7c2cd51b4b6..20b22c3eff22 100644 --- a/packages/next/server/render-result.ts +++ b/packages/next/server/render-result.ts @@ -1,10 +1,21 @@ import type { ServerResponse } from 'http' +type ContentTypeOption = string | undefined + export default class RenderResult { - _result: string | ReadableStream + private _result: string | ReadableStream + private _contentType: ContentTypeOption - constructor(response: string | ReadableStream) { + constructor( + response: string | ReadableStream, + { contentType }: { contentType?: ContentTypeOption } = {} + ) { this._result = response + this._contentType = contentType + } + + contentType(): ContentTypeOption { + return this._contentType } toUnchunkedString(): string { diff --git a/packages/next/server/send-payload/index.ts b/packages/next/server/send-payload/index.ts index ed4b3e78d3c5..fc2be15b2344 100644 --- a/packages/next/server/send-payload/index.ts +++ b/packages/next/server/send-payload/index.ts @@ -71,10 +71,16 @@ export async function sendRenderResult({ } } + const resultContentType = result.contentType() + if (!res.getHeader('Content-Type')) { res.setHeader( 'Content-Type', - type === 'json' ? 'application/json' : 'text/html; charset=utf-8' + resultContentType + ? resultContentType + : type === 'json' + ? 'application/json' + : 'text/html; charset=utf-8' ) } diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index db19c6ce486d..2326ec5bf813 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -370,10 +370,14 @@ export default class NextWebServer extends BaseServer { if (options.poweredByHeader && options.type === 'html') { res.setHeader('X-Powered-By', 'Next.js') } + const resultContentType = options.result.contentType() + if (!res.getHeader('Content-Type')) { res.setHeader( 'Content-Type', - options.type === 'json' + resultContentType + ? resultContentType + : options.type === 'json' ? 'application/json' : 'text/html; charset=utf-8' ) diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index e56701d81b99..5d62435fb27d 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -44,6 +44,19 @@ describe('app dir', () => { }) afterAll(() => next.destroy()) + it('should use application/octet-stream for flight', async () => { + const res = await fetchViaHTTP( + next.url, + '/dashboard/deployments/123?__flight__' + ) + expect(res.headers.get('Content-Type')).toBe('application/octet-stream') + }) + + it('should use application/octet-stream for flight with edge runtime', async () => { + const res = await fetchViaHTTP(next.url, '/dashboard?__flight__') + expect(res.headers.get('Content-Type')).toBe('application/octet-stream') + }) + it('should pass props from getServerSideProps in root layout', async () => { const html = await renderViaHTTP(next.url, '/dashboard') const $ = cheerio.load(html)