Skip to content

Commit b5db704

Browse files
ijjkztanner
authored andcommittedAug 26, 2024
Refactor internal routing headers to use request meta (#66987)
This refactors our handling of passing routing information to the render logic via headers which is legacy from when we had separate routing and render workers. Now this will just attach this meta in our normal request meta handling which is more consistent and type safe.

File tree

8 files changed

+76
-121
lines changed

8 files changed

+76
-121
lines changed
 

‎packages/next/src/server/base-server.ts

+16-54
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ import {
121121
} from './web/spec-extension/adapters/next-request'
122122
import { matchNextDataPathname } from './lib/match-next-data-pathname'
123123
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
124-
import { stripInternalHeaders } from './internal-utils'
125124
import { RSCPathnameNormalizer } from './future/normalizers/request/rsc'
126125
import { PostponedPathnameNormalizer } from './future/normalizers/request/postponed'
127126
import { ActionPathnameNormalizer } from './future/normalizers/request/action'
@@ -621,7 +620,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
621620
// Ignore if its a middleware request when we aren't on edge.
622621
if (
623622
process.env.NEXT_RUNTIME !== 'edge' &&
624-
req.headers['x-middleware-invoke']
623+
getRequestMeta(req, 'middlewareInvoke')
625624
) {
626625
return false
627626
}
@@ -941,7 +940,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
941940
const useMatchedPathHeader =
942941
this.minimalMode && typeof req.headers['x-matched-path'] === 'string'
943942

944-
// TODO: merge handling with x-invoke-path
943+
// TODO: merge handling with invokePath
945944
if (useMatchedPathHeader) {
946945
try {
947946
if (this.enabledDirectories.app) {
@@ -1245,35 +1244,26 @@ export default abstract class Server<ServerOptions extends Options = Options> {
12451244
;(globalThis as any).__incrementalCache = incrementalCache
12461245
}
12471246

1248-
// when x-invoke-path is specified we can short short circuit resolving
1247+
// when invokePath is specified we can short short circuit resolving
12491248
// we only honor this header if we are inside of a render worker to
12501249
// prevent external users coercing the routing path
1251-
const invokePath = req.headers['x-invoke-path'] as string
1250+
const invokePath = getRequestMeta(req, 'invokePath')
12521251
const useInvokePath =
12531252
!useMatchedPathHeader &&
12541253
process.env.NEXT_RUNTIME !== 'edge' &&
12551254
invokePath
12561255

12571256
if (useInvokePath) {
1258-
if (req.headers['x-invoke-status']) {
1259-
const invokeQuery = req.headers['x-invoke-query']
1257+
const invokeStatus = getRequestMeta(req, 'invokeStatus')
1258+
if (invokeStatus) {
1259+
const invokeQuery = getRequestMeta(req, 'invokeQuery')
12601260

1261-
if (typeof invokeQuery === 'string') {
1262-
Object.assign(
1263-
parsedUrl.query,
1264-
JSON.parse(decodeURIComponent(invokeQuery))
1265-
)
1261+
if (invokeQuery) {
1262+
Object.assign(parsedUrl.query, invokeQuery)
12661263
}
12671264

1268-
res.statusCode = Number(req.headers['x-invoke-status'])
1269-
let err: Error | null = null
1270-
1271-
if (typeof req.headers['x-invoke-error'] === 'string') {
1272-
const invokeError = JSON.parse(
1273-
req.headers['x-invoke-error'] || '{}'
1274-
)
1275-
err = new Error(invokeError.message)
1276-
}
1265+
res.statusCode = invokeStatus
1266+
let err: Error | null = getRequestMeta(req, 'invokeError') || null
12771267

12781268
return this.renderError(err, req, res, '/_error', parsedUrl.query)
12791269
}
@@ -1310,13 +1300,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13101300
delete parsedUrl.query[key]
13111301
}
13121302
}
1313-
const invokeQuery = req.headers['x-invoke-query']
1303+
const invokeQuery = getRequestMeta(req, 'invokeQuery')
13141304

1315-
if (typeof invokeQuery === 'string') {
1316-
Object.assign(
1317-
parsedUrl.query,
1318-
JSON.parse(decodeURIComponent(invokeQuery))
1319-
)
1305+
if (invokeQuery) {
1306+
Object.assign(parsedUrl.query, invokeQuery)
13201307
}
13211308

13221309
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
@@ -1328,7 +1315,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13281315

13291316
if (
13301317
process.env.NEXT_RUNTIME !== 'edge' &&
1331-
req.headers['x-middleware-invoke']
1318+
getRequestMeta(req, 'middlewareInvoke')
13321319
) {
13331320
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
13341321
if (finished) return
@@ -1693,28 +1680,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
16931680
)
16941681
}
16951682

1696-
protected stripInternalHeaders(req: BaseNextRequest): void {
1697-
// Skip stripping internal headers in test mode while the header stripping
1698-
// has been explicitly disabled. This allows tests to verify internal
1699-
// routing behavior.
1700-
if (
1701-
process.env.__NEXT_TEST_MODE &&
1702-
process.env.__NEXT_NO_STRIP_INTERNAL_HEADERS === '1'
1703-
) {
1704-
return
1705-
}
1706-
1707-
// Strip the internal headers from both the request and the original
1708-
// request.
1709-
stripInternalHeaders(req.headers)
1710-
if (
1711-
'originalRequest' in req &&
1712-
'headers' in (req as NodeNextRequest).originalRequest
1713-
) {
1714-
stripInternalHeaders((req as NodeNextRequest).originalRequest.headers)
1715-
}
1716-
}
1717-
17181683
protected pathCouldBeIntercepted(resolvedPathname: string): boolean {
17191684
return (
17201685
isInterceptionRouteAppPath(resolvedPathname) ||
@@ -1762,9 +1727,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
17621727
}
17631728
const is404Page = pathname === '/404'
17641729

1765-
// Strip the internal headers.
1766-
this.stripInternalHeaders(req)
1767-
17681730
const is500Page = pathname === '/500'
17691731
const isAppPath = components.isAppPath === true
17701732

@@ -3089,7 +3051,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
30893051
for await (const match of this.matchers.matchAll(pathname, options)) {
30903052
// when a specific invoke-output is meant to be matched
30913053
// ensure a prior dynamic route/page doesn't take priority
3092-
const invokeOutput = ctx.req.headers['x-invoke-output']
3054+
const invokeOutput = getRequestMeta(ctx.req, 'invokeOutput')
30933055
if (
30943056
!this.minimalMode &&
30953057
typeof invokeOutput === 'string' &&

‎packages/next/src/server/internal-utils.ts

-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import type { IncomingHttpHeaders } from 'http'
21
import type { NextParsedUrlQuery } from './request-meta'
32

43
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'
5-
import { INTERNAL_HEADERS } from '../shared/lib/constants'
64

75
const INTERNAL_QUERY_NAMES = [
86
'__nextFallback',
@@ -39,14 +37,3 @@ export function stripInternalSearchParams<T extends string | URL>(
3937

4038
return (isStringUrl ? instance.toString() : instance) as T
4139
}
42-
43-
/**
44-
* Strip internal headers from the request headers.
45-
*
46-
* @param headers the headers to strip of internal headers
47-
*/
48-
export function stripInternalHeaders(headers: IncomingHttpHeaders) {
49-
for (const key of INTERNAL_HEADERS) {
50-
delete headers[key]
51-
}
52-
}

‎packages/next/src/server/lib/router-server.ts

+25-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// this must come first as it includes require hooks
22
import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types'
33
import type { DevBundler } from './router-utils/setup-dev-bundler'
4-
import type { NextUrlWithParsedQuery } from '../request-meta'
4+
import type { NextUrlWithParsedQuery, RequestMeta } from '../request-meta'
55
import type { NextServer } from '../next'
66

77
// This is required before other imports to ensure the require hook is setup.
@@ -19,7 +19,7 @@ import { setupFsCheck } from './router-utils/filesystem'
1919
import { proxyRequest } from './router-utils/proxy-request'
2020
import { isAbortError, pipeToNodeResponse } from '../pipe-readable'
2121
import { getResolveRoutes } from './router-utils/resolve-routes'
22-
import { getRequestMeta } from '../request-meta'
22+
import { addRequestMeta, getRequestMeta } from '../request-meta'
2323
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
2424
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
2525
import setupCompression from 'next/dist/compiled/compression'
@@ -216,7 +216,7 @@ export async function initialize(opts: {
216216
parsedUrl: NextUrlWithParsedQuery,
217217
invokePath: string,
218218
handleIndex: number,
219-
additionalInvokeHeaders: Record<string, string> = {}
219+
additionalRequestMeta?: RequestMeta
220220
) {
221221
// invokeRender expects /api routes to not be locale prefixed
222222
// so normalize here before continuing
@@ -247,16 +247,19 @@ export async function initialize(opts: {
247247
throw new Error('Failed to initialize render server')
248248
}
249249

250-
const invokeHeaders: typeof req.headers = {
251-
...req.headers,
252-
'x-middleware-invoke': '',
253-
'x-invoke-path': invokePath,
254-
'x-invoke-query': encodeURIComponent(JSON.stringify(parsedUrl.query)),
255-
...(additionalInvokeHeaders || {}),
250+
addRequestMeta(req, 'invokePath', invokePath)
251+
addRequestMeta(req, 'invokeQuery', parsedUrl.query)
252+
addRequestMeta(req, 'middlewareInvoke', false)
253+
254+
for (const key in additionalRequestMeta || {}) {
255+
addRequestMeta(
256+
req,
257+
key as keyof RequestMeta,
258+
additionalRequestMeta![key as keyof RequestMeta]
259+
)
256260
}
257-
Object.assign(req.headers, invokeHeaders)
258261

259-
debug('invokeRender', req.url, invokeHeaders)
262+
debug('invokeRender', req.url, req.headers)
260263

261264
try {
262265
const initResult = await renderServer?.instance?.initialize(
@@ -404,10 +407,10 @@ export async function initialize(opts: {
404407
) {
405408
res.statusCode = 500
406409
await invokeRender(parsedUrl, '/_error', handleIndex, {
407-
'x-invoke-status': '500',
408-
'x-invoke-error': JSON.stringify({
409-
message: `A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`,
410-
}),
410+
invokeStatus: 500,
411+
invokeError: new Error(
412+
`A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`
413+
),
411414
})
412415
return
413416
}
@@ -433,7 +436,7 @@ export async function initialize(opts: {
433436
'/405',
434437
handleIndex,
435438
{
436-
'x-invoke-status': '405',
439+
invokeStatus: 405,
437440
}
438441
)
439442
}
@@ -491,14 +494,14 @@ export async function initialize(opts: {
491494

492495
if (typeof err.statusCode === 'number') {
493496
const invokePath = `/${err.statusCode}`
494-
const invokeStatus = `${err.statusCode}`
497+
const invokeStatus = err.statusCode
495498
res.statusCode = err.statusCode
496499
return await invokeRender(
497500
url.parse(invokePath, true),
498501
invokePath,
499502
handleIndex,
500503
{
501-
'x-invoke-status': invokeStatus,
504+
invokeStatus,
502505
}
503506
)
504507
}
@@ -514,7 +517,7 @@ export async function initialize(opts: {
514517
parsedUrl.pathname || '/',
515518
handleIndex,
516519
{
517-
'x-invoke-output': matchedOutput.itemPath,
520+
invokeOutput: matchedOutput.itemPath,
518521
}
519522
)
520523
}
@@ -544,13 +547,13 @@ export async function initialize(opts: {
544547
UNDERSCORE_NOT_FOUND_ROUTE,
545548
handleIndex,
546549
{
547-
'x-invoke-status': '404',
550+
invokeStatus: 404,
548551
}
549552
)
550553
}
551554

552555
await invokeRender(parsedUrl, '/404', handleIndex, {
553-
'x-invoke-status': '404',
556+
invokeStatus: 404,
554557
})
555558
}
556559

@@ -569,7 +572,7 @@ export async function initialize(opts: {
569572
}
570573
res.statusCode = Number(invokeStatus)
571574
return await invokeRender(url.parse(invokePath, true), invokePath, 0, {
572-
'x-invoke-status': invokeStatus,
575+
invokeStatus: res.statusCode,
573576
})
574577
} catch (err2) {
575578
console.error(err2)

‎packages/next/src/server/lib/router-utils/resolve-routes.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -460,15 +460,8 @@ export function getResolveRoutes(
460460
throw new Error(`Failed to initialize render server "middleware"`)
461461
}
462462

463-
const invokeHeaders: typeof req.headers = {
464-
'x-invoke-path': '',
465-
'x-invoke-query': '',
466-
'x-invoke-output': '',
467-
'x-middleware-invoke': '1',
468-
}
469-
Object.assign(req.headers, invokeHeaders)
470-
471-
debug('invoking middleware', req.url, invokeHeaders)
463+
addRequestMeta(req, 'middlewareInvoke', true)
464+
debug('invoking middleware', req.url, req.headers)
472465

473466
let middlewareRes: Response | undefined = undefined
474467
let bodyStream: ReadableStream | undefined = undefined
@@ -573,9 +566,6 @@ export function getResolveRoutes(
573566
'x-middleware-rewrite',
574567
'x-middleware-redirect',
575568
'x-middleware-refresh',
576-
'x-middleware-invoke',
577-
'x-invoke-path',
578-
'x-invoke-query',
579569
].includes(key)
580570
) {
581571
continue

‎packages/next/src/server/next-server.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,7 @@ export default class NextNodeServer extends BaseServer {
11331133
'originalResponse' in _res ? _res.originalResponse : _res
11341134

11351135
const reqStart = Date.now()
1136-
const isMiddlewareRequest = req.headers['x-middleware-invoke']
1136+
const isMiddlewareRequest = getRequestMeta(req, 'middlewareInvoke')
11371137

11381138
const reqCallback = () => {
11391139
// we don't log for non-route requests
@@ -1643,14 +1643,14 @@ export default class NextNodeServer extends BaseServer {
16431643
res: BaseNextResponse,
16441644
parsed: NextUrlWithParsedQuery
16451645
) => {
1646-
const isMiddlewareInvoke = req.headers['x-middleware-invoke']
1646+
const isMiddlewareInvoke = getRequestMeta(req, 'middlewareInvoke')
16471647

16481648
if (!isMiddlewareInvoke) {
16491649
return false
16501650
}
16511651

16521652
const handleFinished = () => {
1653-
res.setHeader('x-middleware-invoke', '1')
1653+
addRequestMeta(req, 'middlewareInvoke', true)
16541654
res.body('').send()
16551655
return true
16561656
}
@@ -1678,9 +1678,6 @@ export default class NextNodeServer extends BaseServer {
16781678
>
16791679
let bubblingResult = false
16801680

1681-
// Strip the internal headers.
1682-
this.stripInternalHeaders(req)
1683-
16841681
try {
16851682
await this.ensureMiddleware(req.url)
16861683

‎packages/next/src/server/request-meta.ts

+30
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,36 @@ export interface RequestMeta {
9999
* The previous revalidate before rendering 404 page for notFound: true
100100
*/
101101
notFoundRevalidate?: number | false
102+
103+
/**
104+
* The path we routed to and should be invoked
105+
*/
106+
invokePath?: string
107+
108+
/**
109+
* The specific page output we should be matching
110+
*/
111+
invokeOutput?: string
112+
113+
/**
114+
* The status we are invoking the request with from routing
115+
*/
116+
invokeStatus?: number
117+
118+
/**
119+
* The routing error we are invoking with
120+
*/
121+
invokeError?: Error
122+
123+
/**
124+
* The query parsed for the invocation
125+
*/
126+
invokeQuery?: Record<string, undefined | string | string[]>
127+
128+
/**
129+
* Whether the request is a middleware invocation
130+
*/
131+
middlewareInvoke?: boolean
102132
}
103133

104134
/**

‎packages/next/src/shared/lib/constants.ts

-13
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,6 @@ export const COMPILER_NAMES = {
1010
edgeServer: 'edge-server',
1111
} as const
1212

13-
/**
14-
* Headers that are set by the Next.js server and should be stripped from the
15-
* request headers going to the user's application.
16-
*/
17-
export const INTERNAL_HEADERS = [
18-
'x-invoke-error',
19-
'x-invoke-output',
20-
'x-invoke-path',
21-
'x-invoke-query',
22-
'x-invoke-status',
23-
'x-middleware-invoke',
24-
] as const
25-
2613
export type CompilerNameValues = ValueOf<typeof COMPILER_NAMES>
2714

2815
export const COMPILER_INDEXES: {

‎test/e2e/i18n-data-route/i18n-data-route.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const pages = [
1010
function checkDataRoute(data: any, page: string) {
1111
expect(data).toHaveProperty('pageProps')
1212
expect(data.pageProps).toHaveProperty('page', page)
13-
expect(data.pageProps).toHaveProperty('output', page)
1413
}
1514

1615
createNextDescribe(

0 commit comments

Comments
 (0)
Please sign in to comment.