diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 766d84d0e4269e7..ae12b0335b5b404 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 e7c2cd51b4b634f..20b22c3eff2289c 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 ed4b3e78d3c5967..fc2be15b2344567 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 db19c6ce486d4b8..2326ec5bf8136ac 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 e56701d81b996d6..5d62435fb27d0c2 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)