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.
This commit is contained in:
JJ Kasper 2024-06-18 05:59:36 -07:00 committed by GitHub
parent 6a18991d3e
commit 61ee393fb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 77 additions and 125 deletions

View file

@ -129,7 +129,6 @@ import {
} from './web/spec-extension/adapters/next-request'
import { matchNextDataPathname } from './lib/match-next-data-pathname'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import { stripInternalHeaders } from './internal-utils'
import { RSCPathnameNormalizer } from './normalizers/request/rsc'
import { PostponedPathnameNormalizer } from './normalizers/request/postponed'
import { ActionPathnameNormalizer } from './normalizers/request/action'
@ -676,7 +675,7 @@ export default abstract class Server<
// Ignore if its a middleware request when we aren't on edge.
if (
process.env.NEXT_RUNTIME !== 'edge' &&
req.headers['x-middleware-invoke']
getRequestMeta(req, 'middlewareInvoke')
) {
return false
}
@ -988,7 +987,7 @@ export default abstract class Server<
const useMatchedPathHeader =
this.minimalMode && typeof req.headers['x-matched-path'] === 'string'
// TODO: merge handling with x-invoke-path
// TODO: merge handling with invokePath
if (useMatchedPathHeader) {
try {
if (this.enabledDirectories.app) {
@ -1310,35 +1309,26 @@ export default abstract class Server<
;(globalThis as any).__incrementalCache = incrementalCache
}
// when x-invoke-path is specified we can short short circuit resolving
// when invokePath is specified we can short short circuit resolving
// we only honor this header if we are inside of a render worker to
// prevent external users coercing the routing path
const invokePath = req.headers['x-invoke-path'] as string
const invokePath = getRequestMeta(req, 'invokePath')
const useInvokePath =
!useMatchedPathHeader &&
process.env.NEXT_RUNTIME !== 'edge' &&
invokePath
if (useInvokePath) {
if (req.headers['x-invoke-status']) {
const invokeQuery = req.headers['x-invoke-query']
const invokeStatus = getRequestMeta(req, 'invokeStatus')
if (invokeStatus) {
const invokeQuery = getRequestMeta(req, 'invokeQuery')
if (typeof invokeQuery === 'string') {
Object.assign(
parsedUrl.query,
JSON.parse(decodeURIComponent(invokeQuery))
)
if (invokeQuery) {
Object.assign(parsedUrl.query, invokeQuery)
}
res.statusCode = Number(req.headers['x-invoke-status'])
let err: Error | null = null
if (typeof req.headers['x-invoke-error'] === 'string') {
const invokeError = JSON.parse(
req.headers['x-invoke-error'] || '{}'
)
err = new Error(invokeError.message)
}
res.statusCode = invokeStatus
let err: Error | null = getRequestMeta(req, 'invokeError') || null
return this.renderError(err, req, res, '/_error', parsedUrl.query)
}
@ -1375,13 +1365,10 @@ export default abstract class Server<
delete parsedUrl.query[key]
}
}
const invokeQuery = req.headers['x-invoke-query']
const invokeQuery = getRequestMeta(req, 'invokeQuery')
if (typeof invokeQuery === 'string') {
Object.assign(
parsedUrl.query,
JSON.parse(decodeURIComponent(invokeQuery))
)
if (invokeQuery) {
Object.assign(parsedUrl.query, invokeQuery)
}
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
@ -1393,7 +1380,7 @@ export default abstract class Server<
if (
process.env.NEXT_RUNTIME !== 'edge' &&
req.headers['x-middleware-invoke']
getRequestMeta(req, 'middlewareInvoke')
) {
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
if (finished) return
@ -1817,31 +1804,6 @@ export default abstract class Server<
)
}
protected stripInternalHeaders(req: ServerRequest): void {
// Skip stripping internal headers in test mode while the header stripping
// has been explicitly disabled. This allows tests to verify internal
// routing behavior.
if (
process.env.__NEXT_TEST_MODE &&
process.env.__NEXT_NO_STRIP_INTERNAL_HEADERS === '1'
) {
return
}
// Strip the internal headers from both the request and the original
// request.
stripInternalHeaders(req.headers)
if (
// The type check here ensures that `req` is correctly typed, and the
// environment variable check provides dead code elimination.
process.env.NEXT_RUNTIME !== 'edge' &&
isNodeNextRequest(req)
) {
stripInternalHeaders(req.originalRequest.headers)
}
}
protected pathCouldBeIntercepted(resolvedPathname: string): boolean {
return (
isInterceptionRouteAppPath(resolvedPathname) ||
@ -1894,9 +1856,6 @@ export default abstract class Server<
}
const is404Page = pathname === '/404'
// Strip the internal headers.
this.stripInternalHeaders(req)
const is500Page = pathname === '/500'
const isAppPath = components.isAppPath === true
@ -3336,7 +3295,7 @@ export default abstract class Server<
for await (const match of this.matchers.matchAll(pathname, options)) {
// when a specific invoke-output is meant to be matched
// ensure a prior dynamic route/page doesn't take priority
const invokeOutput = ctx.req.headers['x-invoke-output']
const invokeOutput = getRequestMeta(ctx.req, 'invokeOutput')
if (
!this.minimalMode &&
typeof invokeOutput === 'string' &&

View file

@ -1,8 +1,6 @@
import type { IncomingHttpHeaders } from 'http'
import type { NextParsedUrlQuery } from './request-meta'
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'
import { INTERNAL_HEADERS } from '../shared/lib/constants'
const INTERNAL_QUERY_NAMES = [
'__nextFallback',
@ -39,14 +37,3 @@ export function stripInternalSearchParams<T extends string | URL>(
return (isStringUrl ? instance.toString() : instance) as T
}
/**
* Strip internal headers from the request headers.
*
* @param headers the headers to strip of internal headers
*/
export function stripInternalHeaders(headers: IncomingHttpHeaders) {
for (const key of INTERNAL_HEADERS) {
delete headers[key]
}
}

View file

@ -1,7 +1,7 @@
// this must come first as it includes require hooks
import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types'
import type { DevBundler } from './router-utils/setup-dev-bundler'
import type { NextUrlWithParsedQuery } from '../request-meta'
import type { NextUrlWithParsedQuery, RequestMeta } from '../request-meta'
import type { NextServer } from '../next'
// This is required before other imports to ensure the require hook is setup.
@ -19,7 +19,7 @@ import { setupFsCheck } from './router-utils/filesystem'
import { proxyRequest } from './router-utils/proxy-request'
import { isAbortError, pipeToNodeResponse } from '../pipe-readable'
import { getResolveRoutes } from './router-utils/resolve-routes'
import { getRequestMeta } from '../request-meta'
import { addRequestMeta, getRequestMeta } from '../request-meta'
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
import setupCompression from 'next/dist/compiled/compression'
@ -218,7 +218,7 @@ export async function initialize(opts: {
parsedUrl: NextUrlWithParsedQuery,
invokePath: string,
handleIndex: number,
additionalInvokeHeaders: Record<string, string> = {}
additionalRequestMeta?: RequestMeta
) {
// invokeRender expects /api routes to not be locale prefixed
// so normalize here before continuing
@ -249,16 +249,19 @@ export async function initialize(opts: {
throw new Error('Failed to initialize render server')
}
const invokeHeaders: typeof req.headers = {
...req.headers,
'x-middleware-invoke': '',
'x-invoke-path': invokePath,
'x-invoke-query': encodeURIComponent(JSON.stringify(parsedUrl.query)),
...(additionalInvokeHeaders || {}),
}
Object.assign(req.headers, invokeHeaders)
addRequestMeta(req, 'invokePath', invokePath)
addRequestMeta(req, 'invokeQuery', parsedUrl.query)
addRequestMeta(req, 'middlewareInvoke', false)
debug('invokeRender', req.url, invokeHeaders)
for (const key in additionalRequestMeta || {}) {
addRequestMeta(
req,
key as keyof RequestMeta,
additionalRequestMeta![key as keyof RequestMeta]
)
}
debug('invokeRender', req.url, req.headers)
try {
const initResult =
@ -405,10 +408,10 @@ export async function initialize(opts: {
) {
res.statusCode = 500
await invokeRender(parsedUrl, '/_error', handleIndex, {
'x-invoke-status': '500',
'x-invoke-error': JSON.stringify({
message: `A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`,
}),
invokeStatus: 500,
invokeError: new Error(
`A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`
),
})
return
}
@ -434,7 +437,7 @@ export async function initialize(opts: {
'/405',
handleIndex,
{
'x-invoke-status': '405',
invokeStatus: 405,
}
)
}
@ -492,14 +495,14 @@ export async function initialize(opts: {
if (typeof err.statusCode === 'number') {
const invokePath = `/${err.statusCode}`
const invokeStatus = `${err.statusCode}`
const invokeStatus = err.statusCode
res.statusCode = err.statusCode
return await invokeRender(
url.parse(invokePath, true),
invokePath,
handleIndex,
{
'x-invoke-status': invokeStatus,
invokeStatus,
}
)
}
@ -515,7 +518,7 @@ export async function initialize(opts: {
parsedUrl.pathname || '/',
handleIndex,
{
'x-invoke-output': matchedOutput.itemPath,
invokeOutput: matchedOutput.itemPath,
}
)
}
@ -545,13 +548,13 @@ export async function initialize(opts: {
UNDERSCORE_NOT_FOUND_ROUTE,
handleIndex,
{
'x-invoke-status': '404',
invokeStatus: 404,
}
)
}
await invokeRender(parsedUrl, '/404', handleIndex, {
'x-invoke-status': '404',
invokeStatus: 404,
})
}
@ -570,7 +573,7 @@ export async function initialize(opts: {
}
res.statusCode = Number(invokeStatus)
return await invokeRender(url.parse(invokePath, true), invokePath, 0, {
'x-invoke-status': invokeStatus,
invokeStatus: res.statusCode,
})
} catch (err2) {
console.error(err2)

View file

@ -459,15 +459,8 @@ export function getResolveRoutes(
throw new Error(`Failed to initialize render server "middleware"`)
}
const invokeHeaders: typeof req.headers = {
'x-invoke-path': '',
'x-invoke-query': '',
'x-invoke-output': '',
'x-middleware-invoke': '1',
}
Object.assign(req.headers, invokeHeaders)
debug('invoking middleware', req.url, invokeHeaders)
addRequestMeta(req, 'middlewareInvoke', true)
debug('invoking middleware', req.url, req.headers)
let middlewareRes: Response | undefined = undefined
let bodyStream: ReadableStream | undefined = undefined
@ -572,9 +565,6 @@ export function getResolveRoutes(
'x-middleware-rewrite',
'x-middleware-redirect',
'x-middleware-refresh',
'x-middleware-invoke',
'x-invoke-path',
'x-invoke-query',
].includes(key)
) {
continue

View file

@ -1121,7 +1121,7 @@ export default class NextNodeServer extends BaseServer<
const { originalResponse } = normalizedRes
const reqStart = Date.now()
const isMiddlewareRequest = req.headers['x-middleware-invoke']
const isMiddlewareRequest = getRequestMeta(req, 'middlewareInvoke')
const reqCallback = () => {
// we don't log for non-route requests
@ -1640,14 +1640,14 @@ export default class NextNodeServer extends BaseServer<
res,
parsed
) => {
const isMiddlewareInvoke = req.headers['x-middleware-invoke']
const isMiddlewareInvoke = getRequestMeta(req, 'middlewareInvoke')
if (!isMiddlewareInvoke) {
return false
}
const handleFinished = () => {
res.setHeader('x-middleware-invoke', '1')
addRequestMeta(req, 'middlewareInvoke', true)
res.body('').send()
return true
}
@ -1675,9 +1675,6 @@ export default class NextNodeServer extends BaseServer<
>
let bubblingResult = false
// Strip the internal headers.
this.stripInternalHeaders(req)
try {
await this.ensureMiddleware(req.url)

View file

@ -99,6 +99,36 @@ export interface RequestMeta {
* The previous revalidate before rendering 404 page for notFound: true
*/
notFoundRevalidate?: number | false
/**
* The path we routed to and should be invoked
*/
invokePath?: string
/**
* The specific page output we should be matching
*/
invokeOutput?: string
/**
* The status we are invoking the request with from routing
*/
invokeStatus?: number
/**
* The routing error we are invoking with
*/
invokeError?: Error
/**
* The query parsed for the invocation
*/
invokeQuery?: Record<string, undefined | string | string[]>
/**
* Whether the request is a middleware invocation
*/
middlewareInvoke?: boolean
}
/**

View file

@ -10,19 +10,6 @@ export const COMPILER_NAMES = {
edgeServer: 'edge-server',
} as const
/**
* Headers that are set by the Next.js server and should be stripped from the
* request headers going to the user's application.
*/
export const INTERNAL_HEADERS = [
'x-invoke-error',
'x-invoke-output',
'x-invoke-path',
'x-invoke-query',
'x-invoke-status',
'x-middleware-invoke',
] as const
export type CompilerNameValues = ValueOf<typeof COMPILER_NAMES>
export const COMPILER_INDEXES: {

View file

@ -10,7 +10,6 @@ const pages = [
function checkDataRoute(data: any, page: string) {
expect(data).toHaveProperty('pageProps')
expect(data.pageProps).toHaveProperty('page', page)
expect(data.pageProps).toHaveProperty('output', page)
}
describe('i18n-data-route', () => {