Skip to content

Commit

Permalink
feat: allow error pages to be rendered by the routeModule
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Jun 15, 2023
1 parent 019ed1c commit d3d26a5
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,16 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
${
isAppDir
? `
import { renderToHTMLOrFlight as appRenderToHTML } from 'next/dist/esm/server/app-render/app-render'
import { renderToHTMLOrFlight as renderToHTML } from 'next/dist/esm/server/app-render/app-render'
import * as pageMod from ${JSON.stringify(pageModPath)}
const Document = null
const pagesRenderToHTML = null
const appMod = null
const errorMod = null
const error500Mod = null
`
: `
import Document from ${stringifiedDocumentPath}
import { renderToHTML as pagesRenderToHTML } from 'next/dist/esm/server/render'
import { renderToHTML } from 'next/dist/esm/server/render'
import * as pageMod from ${stringifiedPagePath}
import * as appMod from ${stringifiedAppPath}
import * as errorMod from ${stringifiedErrorPath}
Expand All @@ -135,7 +134,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
? `import * as error500Mod from ${stringified500Path}`
: `const error500Mod = null`
}
const appRenderToHTML = null
`
}
Expand Down Expand Up @@ -171,8 +169,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
buildManifest,
isAppPath: ${!!isAppDir},
prerenderManifest,
appRenderToHTML,
pagesRenderToHTML,
renderToHTML,
reactLoadableManifest,
clientReferenceManifest: ${isServerComponent} ? rscManifest : null,
serverActionsManifest: ${isServerComponent} ? rscServerManifest : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ export function getRender({
buildManifest,
prerenderManifest,
reactLoadableManifest,
appRenderToHTML,
pagesRenderToHTML,
renderToHTML,
clientReferenceManifest,
subresourceIntegrityManifest,
serverActionsManifest,
Expand All @@ -44,8 +43,7 @@ export function getRender({
pageMod: any
errorMod: any
error500Mod: any
appRenderToHTML: any
pagesRenderToHTML: any
renderToHTML: any
Document: DocumentType
buildManifest: BuildManifest
prerenderManifest: PrerenderManifest
Expand Down Expand Up @@ -88,8 +86,7 @@ export function getRender({
clientReferenceManifest,
serverActionsManifest,
},
appRenderToHTML,
pagesRenderToHTML,
renderToHTML,
incrementalCacheHandler,
loadComponent: async (pathname) => {
if (pathname === page) {
Expand Down Expand Up @@ -137,12 +134,15 @@ export function getRender({
},
},
})
const requestHandler = server.getRequestHandler()

const handler = server.getRequestHandler()

return async function render(request: Request) {
const extendedReq = new WebNextRequest(request)
const extendedRes = new WebNextResponse()
requestHandler(extendedReq, extendedRes)

handler(extendedReq, extendedRes)

return await extendedRes.toResponse()
}
}
56 changes: 36 additions & 20 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ import * as Log from '../build/output/log'
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
import { getUtils } from './server-utils'
import isError, { getProperError } from '../lib/is-error'
import { addRequestMeta, getRequestMeta } from './request-meta'
import {
addRequestMeta,
getRequestMeta,
removeRequestMeta,
} from './request-meta'

import { ImageConfigComplete } from '../shared/lib/image-config'
import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix'
Expand Down Expand Up @@ -549,8 +553,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
res: BaseNextResponse,
parsedUrl?: NextUrlWithParsedQuery
): Promise<void> {
await this.prepare()
// Wait until all async tasks are completed by the server.
await Promise.all([this.prepare(), this.matchers.waitTillReady()])

const method = req.method.toUpperCase()

return getTracer().trace(
BaseServerSpan.handleRequest,
{
Expand All @@ -563,8 +570,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// We will fire this from the renderer worker
hideSpan: this.isRouterWorker,
},
async (span) =>
this.handleRequestImpl(req, res, parsedUrl).finally(() => {
async (span) => {
try {
await this.handleRequestImpl(req, res, parsedUrl)
} finally {
if (!span) return
span.setAttributes({
'http.status_code': res.statusCode,
Expand Down Expand Up @@ -595,7 +604,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
})
span.updateName(newName)
}
})
}
}
)
}

Expand All @@ -605,9 +615,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
parsedUrl?: NextUrlWithParsedQuery
): Promise<void> {
try {
// Wait for the matchers to be ready.
await this.matchers.waitTillReady()

// ensure cookies set in middleware are merged and
// not overridden by API routes/getServerSideProps
const _res = (res as any).originalResponse || res
Expand Down Expand Up @@ -638,15 +645,15 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return origSetHeader(name, val)
}

const urlParts = (req.url || '').split('?')
const urlParts = req.url.split('?')
const urlNoQuery = urlParts[0]

// this normalizes repeated slashes in the path e.g. hello//world ->
// hello/world or backslashes to forward slashes, this does not
// handle trailing slash as that is handled the same as a next.config.js
// redirect
if (urlNoQuery?.match(/(\\|\/\/)/)) {
const cleanUrl = normalizeRepeatedSlashes(req.url!)
const cleanUrl = normalizeRepeatedSlashes(req.url)
res.redirect(cleanUrl, 308).body(cleanUrl).send()
return
}
Expand All @@ -655,7 +662,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {

// Parse url if parsedUrl not provided
if (!parsedUrl || typeof parsedUrl !== 'object') {
parsedUrl = parseUrl(req.url!, true)
parsedUrl = parseUrl(req.url, true)
}

// Parse the querystring ourselves if the user doesn't handle querystring parsing
Expand Down Expand Up @@ -700,7 +707,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
url.pathname = pathnameInfo.pathname

if (pathnameInfo.basePath) {
req.url = removePathPrefix(req.url!, this.nextConfig.basePath)
req.url = removePathPrefix(req.url, this.nextConfig.basePath)
addRequestMeta(req, '_nextHadBasePath', true)
}

Expand Down Expand Up @@ -896,7 +903,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {

if (params) {
matchedPath = utils.interpolateDynamicPath(srcPathname, params)
req.url = utils.interpolateDynamicPath(req.url!, params)
req.url = utils.interpolateDynamicPath(req.url, params)
}
}

Expand Down Expand Up @@ -1216,7 +1223,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
!this.minimalMode &&
!query.__nextDataReq &&
(req.url?.match(/^\/_next\//) ||
(this.hasStaticDir && req.url!.match(/^\/static\//)))
(this.hasStaticDir && req.url.match(/^\/static\//)))
) {
return this.handleRequest(req, res, parsedUrl)
}
Expand Down Expand Up @@ -1674,11 +1681,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// served by the server.
let result: RenderResult

// We want to use the match when we're not trying to render an error page.
const match =
pathname !== '/_error' && !is404Page && !is500Page
? getRequestMeta(req, '_nextMatch')
: undefined
// Get the request match for this path. If an error is being rendered, the
// match for the error page will be returned.
const match = getRequestMeta(req, '_nextMatch')

if (
match &&
Expand Down Expand Up @@ -2529,6 +2534,17 @@ export default abstract class Server<ServerOptions extends Options = Options> {
)
}

// If the page has a route module, use it for the new match. If it doesn't
// have a route module, remove the match.
if (result.components.ComponentMod.routeModule) {
addRequestMeta(ctx.req, '_nextMatch', {
definition: result.components.ComponentMod.routeModule.definition,
params: undefined,
})
} else {
removeRequestMeta(ctx.req, '_nextMatch')
}

try {
return await this.renderToResponseWithComponents(
{
Expand Down Expand Up @@ -2609,7 +2625,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
parsedUrl?: Pick<NextUrlWithParsedQuery, 'pathname' | 'query'>,
setHeaders = true
): Promise<void> {
const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(req.url!, true)
const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(req.url, true)

if (this.nextConfig.i18n) {
query.__nextLocale ||= this.nextConfig.i18n.defaultLocale
Expand Down
41 changes: 40 additions & 1 deletion packages/next/src/server/request-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export interface RequestMeta {
_nextMinimalMode?: boolean
}

/**
* Gets the request metadata. If no key is provided, the entire metadata object
* is returned.
*
* @param req the request to get the metadata from
* @param key the key to get from the metadata (optional)
* @returns the value for the key or the entire metadata object
*/
export function getRequestMeta(
req: NextIncomingMessage,
key?: undefined
Expand All @@ -58,11 +66,26 @@ export function getRequestMeta<K extends keyof RequestMeta>(
return typeof key === 'string' ? meta[key] : meta
}

/**
* Sets the request metadata.
*
* @param req the request to set the metadata on
* @param meta the metadata to set
* @returns the mutated request metadata
*/
export function setRequestMeta(req: NextIncomingMessage, meta: RequestMeta) {
req[NEXT_REQUEST_META] = meta
return getRequestMeta(req)
return meta
}

/**
* Adds a value to the request metadata.
*
* @param request the request to mutate
* @param key the key to set
* @param value the value to set
* @returns the mutated request metadata
*/
export function addRequestMeta<K extends keyof RequestMeta>(
request: NextIncomingMessage,
key: K,
Expand All @@ -73,6 +96,22 @@ export function addRequestMeta<K extends keyof RequestMeta>(
return setRequestMeta(request, meta)
}

/**
* Removes a key from the request metadata.
*
* @param request the request to mutate
* @param key the key to remove
* @returns the mutated request metadata
*/
export function removeRequestMeta<K extends keyof RequestMeta>(
request: NextIncomingMessage,
key: K
) {
const meta = getRequestMeta(request)
delete meta[key]
return setRequestMeta(request, meta)
}

type NextQueryMetadata = {
__nextNotFoundSrcPage?: string
__nextDefaultLocale?: string
Expand Down
38 changes: 17 additions & 21 deletions packages/next/src/server/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ interface WebServerOptions extends Options {
) => Promise<LoadComponentsReturnType | null>
extendRenderOpts: Partial<BaseServer['renderOpts']> &
Pick<BaseServer['renderOpts'], 'buildId'>
pagesRenderToHTML?: typeof import('./render').renderToHTML
appRenderToHTML?: typeof import('./app-render/app-render').renderToHTMLOrFlight
renderToHTML:
| typeof import('./render').renderToHTML
| typeof import('./app-render/app-render').renderToHTMLOrFlight
incrementalCacheHandler?: any
prerenderManifest: PrerenderManifest | undefined
}
Expand Down Expand Up @@ -362,32 +363,27 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
return false
}

protected async renderHTML(
protected renderHTML(
req: WebNextRequest,
res: WebNextResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts
): Promise<RenderResult> {
const { pagesRenderToHTML, appRenderToHTML } =
this.serverOptions.webServerConfig
const curRenderToHTML = pagesRenderToHTML || appRenderToHTML

if (curRenderToHTML) {
return await curRenderToHTML(
req as any,
res as any,
pathname,
query,
Object.assign(renderOpts, {
disableOptimizedLoading: true,
runtime: 'experimental-edge',
})
)
} else {
throw new Error(`Invariant: curRenderToHTML is missing`)
}
const { renderToHTML } = this.serverOptions.webServerConfig

return renderToHTML(
req as any,
res as any,
pathname,
query,
Object.assign(renderOpts, {
disableOptimizedLoading: true,
runtime: 'experimental-edge',
})
)
}

protected async sendRenderResult(
_req: WebNextRequest,
res: WebNextResponse,
Expand Down

0 comments on commit d3d26a5

Please sign in to comment.