Skip to content

Commit

Permalink
Refactor internal routing headers to use request meta (#66987)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ijjk authored Jun 18, 2024
1 parent 6a18991 commit 61ee393
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 124 deletions.
73 changes: 16 additions & 57 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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' &&
Expand Down
13 changes: 0 additions & 13 deletions packages/next/src/server/internal-utils.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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]
}
}
47 changes: 25 additions & 22 deletions packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 || {}),
addRequestMeta(req, 'invokePath', invokePath)
addRequestMeta(req, 'invokeQuery', parsedUrl.query)
addRequestMeta(req, 'middlewareInvoke', false)

for (const key in additionalRequestMeta || {}) {
addRequestMeta(
req,
key as keyof RequestMeta,
additionalRequestMeta![key as keyof RequestMeta]
)
}
Object.assign(req.headers, invokeHeaders)

debug('invokeRender', req.url, invokeHeaders)
debug('invokeRender', req.url, req.headers)

try {
const initResult =
Expand Down Expand Up @@ -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
}
Expand All @@ -434,7 +437,7 @@ export async function initialize(opts: {
'/405',
handleIndex,
{
'x-invoke-status': '405',
invokeStatus: 405,
}
)
}
Expand Down Expand Up @@ -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,
}
)
}
Expand All @@ -515,7 +518,7 @@ export async function initialize(opts: {
parsedUrl.pathname || '/',
handleIndex,
{
'x-invoke-output': matchedOutput.itemPath,
invokeOutput: matchedOutput.itemPath,
}
)
}
Expand Down Expand Up @@ -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,
})
}

Expand All @@ -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)
Expand Down
14 changes: 2 additions & 12 deletions packages/next/src/server/lib/router-utils/resolve-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down
Loading

0 comments on commit 61ee393

Please sign in to comment.