Skip to content

Commit d41ca43

Browse files
authored
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)
1 parent 1bf7d4d commit d41ca43

File tree

5 files changed

+49
-6
lines changed

5 files changed

+49
-6
lines changed

packages/next/server/app-render.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export type RenderOptsPartial = {
5151

5252
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
5353

54+
/**
55+
* Flight Response is always set to application/octet-stream to ensure it does not
56+
*/
57+
class FlightRenderResult extends RenderResult {
58+
constructor(response: string | ReadableStream<Uint8Array>) {
59+
super(response, { contentType: 'application/octet-stream' })
60+
}
61+
}
62+
5463
/**
5564
* Interop between "export default" and "module.exports".
5665
*/
@@ -500,7 +509,7 @@ export async function renderToHTMLOrFlight(
500509

501510
// Empty so that the client-side router will do a full page navigation.
502511
const flightData: FlightData = pathname + (search ? `?${search}` : '')
503-
return new RenderResult(
512+
return new FlightRenderResult(
504513
renderToReadableStream(flightData, serverComponentManifest).pipeThrough(
505514
createBufferedTransformStream()
506515
)
@@ -1054,7 +1063,7 @@ export async function renderToHTMLOrFlight(
10541063
).slice(1),
10551064
]
10561065

1057-
return new RenderResult(
1066+
return new FlightRenderResult(
10581067
renderToReadableStream(flightData, serverComponentManifest, {
10591068
context: serverContexts,
10601069
}).pipeThrough(createBufferedTransformStream())

packages/next/server/render-result.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import type { ServerResponse } from 'http'
22

3+
type ContentTypeOption = string | undefined
4+
35
export default class RenderResult {
4-
_result: string | ReadableStream<Uint8Array>
6+
private _result: string | ReadableStream<Uint8Array>
7+
private _contentType: ContentTypeOption
58

6-
constructor(response: string | ReadableStream<Uint8Array>) {
9+
constructor(
10+
response: string | ReadableStream<Uint8Array>,
11+
{ contentType }: { contentType?: ContentTypeOption } = {}
12+
) {
713
this._result = response
14+
this._contentType = contentType
15+
}
16+
17+
contentType(): ContentTypeOption {
18+
return this._contentType
819
}
920

1021
toUnchunkedString(): string {

packages/next/server/send-payload/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,16 @@ export async function sendRenderResult({
7171
}
7272
}
7373

74+
const resultContentType = result.contentType()
75+
7476
if (!res.getHeader('Content-Type')) {
7577
res.setHeader(
7678
'Content-Type',
77-
type === 'json' ? 'application/json' : 'text/html; charset=utf-8'
79+
resultContentType
80+
? resultContentType
81+
: type === 'json'
82+
? 'application/json'
83+
: 'text/html; charset=utf-8'
7884
)
7985
}
8086

packages/next/server/web-server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,14 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
370370
if (options.poweredByHeader && options.type === 'html') {
371371
res.setHeader('X-Powered-By', 'Next.js')
372372
}
373+
const resultContentType = options.result.contentType()
374+
373375
if (!res.getHeader('Content-Type')) {
374376
res.setHeader(
375377
'Content-Type',
376-
options.type === 'json'
378+
resultContentType
379+
? resultContentType
380+
: options.type === 'json'
377381
? 'application/json'
378382
: 'text/html; charset=utf-8'
379383
)

test/e2e/app-dir/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ describe('app dir', () => {
4444
})
4545
afterAll(() => next.destroy())
4646

47+
it('should use application/octet-stream for flight', async () => {
48+
const res = await fetchViaHTTP(
49+
next.url,
50+
'/dashboard/deployments/123?__flight__'
51+
)
52+
expect(res.headers.get('Content-Type')).toBe('application/octet-stream')
53+
})
54+
55+
it('should use application/octet-stream for flight with edge runtime', async () => {
56+
const res = await fetchViaHTTP(next.url, '/dashboard?__flight__')
57+
expect(res.headers.get('Content-Type')).toBe('application/octet-stream')
58+
})
59+
4760
it('should pass props from getServerSideProps in root layout', async () => {
4861
const html = await renderViaHTTP(next.url, '/dashboard')
4962
const $ = cheerio.load(html)

0 commit comments

Comments
 (0)