diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index e2c478c0e834c48..21aecb07c28aa91 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -162,6 +162,7 @@ export function createEntrypoints( page, absoluteAppPath: pages['/_app'], absoluteDocumentPath: pages['/_document'], + absoluteErrorPath: pages['/500'] || pages['/_error'], absolutePagePath, isServerComponent: isFlight, buildId, diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 25f664f31b6fc9e..810569b6ecd63bf 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -5,6 +5,7 @@ export default async function middlewareRSCLoader(this: any) { absolutePagePath, absoluteAppPath, absoluteDocumentPath, + absoluteErrorPath, basePath, isServerComponent: isServerComponentQuery, assetPrefix, @@ -14,13 +15,15 @@ export default async function middlewareRSCLoader(this: any) { const isServerComponent = isServerComponentQuery === 'true' const stringifiedAbsolutePagePath = stringifyRequest(this, absolutePagePath) const stringifiedAbsoluteAppPath = stringifyRequest(this, absoluteAppPath) + const stringifiedAbsoluteErrorPath = stringifyRequest(this, absoluteErrorPath) const stringifiedAbsoluteDocumentPath = stringifyRequest( this, absoluteDocumentPath ) - let appDefinition = `const App = require(${stringifiedAbsoluteAppPath}).default` - let documentDefinition = `const Document = require(${stringifiedAbsoluteDocumentPath}).default` + const appDefinition = `const App = require(${stringifiedAbsoluteAppPath}).default` + const documentDefinition = `const Document = require(${stringifiedAbsoluteDocumentPath}).default` + const errorDefinition = `const ErrorPage = require(${stringifiedAbsoluteErrorPath}).default` const transformed = ` import { adapter } from 'next/dist/server/web/adapter' @@ -40,6 +43,7 @@ export default async function middlewareRSCLoader(this: any) { ${appDefinition} ${documentDefinition} + ${errorDefinition} const { default: Page, @@ -134,6 +138,7 @@ export default async function middlewareRSCLoader(this: any) { : '' } + const req = { url: pathname } const renderOpts = { Component, pageConfig: config || {}, @@ -162,32 +167,36 @@ export default async function middlewareRSCLoader(this: any) { const transformStream = new TransformStream() const writer = transformStream.writable.getWriter() const encoder = new TextEncoder() - + let result + let statusCode = 200 try { - const result = await renderToHTML( - { url: pathname }, + result = await renderToHTML( + req, {}, pathname, query, renderOpts ) - result.pipe({ - write: str => writer.write(encoder.encode(str)), - end: () => writer.close() - }) - .catch(() => writer.close()) } catch (err) { - return new Response( - (err || 'An error occurred while rendering ' + pathname + '.').toString(), - { - status: 500, - headers: { 'x-middleware-ssr': '1' } - } + statusCode = 500 + const errorRes = { statusCode, err } + result = await renderToHTML( + req, + errorRes, + pathname, + query, + { ...renderOpts, Component: ErrorPage } ) } + result.pipe({ + write: str => writer.write(encoder.encode(str)), + end: () => writer.close() + }) + return new Response(transformStream.readable, { - headers: { 'x-middleware-ssr': '1' } + headers: { 'x-middleware-ssr': '1' }, + status: statusCode }) } diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 7a0164864c2c5b9..3887dace73edac3 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -528,6 +528,7 @@ export default class HotReloader { page, absoluteAppPath: this.pagesMapping['/_app'], absoluteDocumentPath: this.pagesMapping['/_document'], + absoluteErrorPath: this.pagesMapping['/_error'], absolutePagePath, isServerComponent, buildId: this.buildId, diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 0f8de8ca7da9219..5abb91ccfe401e5 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1433,44 +1433,48 @@ function renderToNodeStream( function renderToReadableStream( element: React.ReactElement -): NodeWritablePiper { - return (res, next) => { - let bufferedString = '' - let shellCompleted = false - - const readable = (ReactDOMServer as any).renderToReadableStream(element, { - onCompleteShell() { - shellCompleted = true - if (bufferedString) { - res.write(bufferedString) - bufferedString = '' - } - }, - }) - const reader = readable.getReader() - const decoder = new TextDecoder() - const process = () => { - reader.read().then( - ({ done, value }: any) => { - if (!done) { - const s = typeof value === 'string' ? value : decoder.decode(value) - if (shellCompleted) { +): Promise { + return new Promise((resolve, reject) => { + let reader: any = null + let resolved = false + const doResolve = (getReader: () => any) => { + if (resolved) return + resolved = true + const piper: NodeWritablePiper = (res, next) => { + const process = async () => { + const reader: ReadableStreamDefaultReader = getReader() + const decoder = new TextDecoder() + reader.read().then(({ done, value }) => { + if (!done) { + const s = + typeof value === 'string' ? value : decoder.decode(value) res.write(s) + process() } else { - bufferedString += s + next() } - process() - } else { - next() - } - }, - (err: any) => { - next(err) + }) } - ) + process() + } + resolve(piper) } - process() - } + + const readable = (ReactDOMServer as any).renderToReadableStream(element, { + onError(err: Error) { + if (!resolved) { + resolved = true + reject(err) + } + }, + onCompleteShell() { + doResolve(() => reader) + }, + }) + // Start reader and lock stream immediately to consume readable, + // Otherwise the bytes before `onCompleteShell` will be missed. + reader = readable.getReader() + }) } function chainPipers(pipers: NodeWritablePiper[]): NodeWritablePiper {