Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 4f35ee7

Browse files
authored
Simplify next-dev-server implementation (vercel#26230)
`next-dev-server` having its own implementations of `renderToHTML` and `renderErrorToHTML` has historically made reasoning about streaming hard, as it adds additional places where status codes are explicitly set and the full HTML is blocked on. Instead, this PR simplifies things considerably by moving the majority of the custom logic for e.g. hot reloading and on-demand compilation to when we're resolving the page to be loaded, rather than upfront when handling the request. It also cleans up a few other details (e.g. default error page rendering) that managed to creep into the base implementation over time. One unfortunate side effect is that this makes compilation errors slightly more difficult. Previously, we'd render them directly. Now, we have to rethrow them. But since they've already been logged (by the watch pipeline), we have to make sure they don't get logged again.
1 parent 0a3d77d commit 4f35ee7

File tree

2 files changed

+80
-102
lines changed

2 files changed

+80
-102
lines changed

packages/next/next-server/server/next-server.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@ import {
5252
import { DomainLocales, isTargetLikeServerless, NextConfig } from './config'
5353
import pathMatch from '../lib/router/utils/path-match'
5454
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
55-
import {
56-
loadComponents,
57-
LoadComponentsReturnType,
58-
loadDefaultErrorComponents,
59-
} from './load-components'
55+
import { loadComponents, LoadComponentsReturnType } from './load-components'
6056
import { normalizePagePath } from './normalize-page-path'
6157
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
6258
import { getPagePath, requireFontManifest } from './require'
@@ -92,7 +88,6 @@ import cookie from 'next/dist/compiled/cookie'
9288
import escapePathDelimiters from '../lib/router/utils/escape-path-delimiters'
9389
import { getUtils } from '../../build/webpack/loaders/next-serverless-loader/utils'
9490
import { PreviewData } from 'next/types'
95-
import HotReloader from '../../server/hot-reloader'
9691

9792
const getCustomRouteMatcher = pathMatch(true)
9893

@@ -102,7 +97,7 @@ type Middleware = (
10297
next: (err?: Error) => void
10398
) => void
10499

105-
type FindComponentsResult = {
100+
export type FindComponentsResult = {
106101
components: LoadComponentsReturnType
107102
query: ParsedUrlQuery
108103
}
@@ -1357,7 +1352,7 @@ export default class Server {
13571352
return this.sendHTML(req, res, html)
13581353
}
13591354

1360-
private async findPageComponents(
1355+
protected async findPageComponents(
13611356
pathname: string,
13621357
query: ParsedUrlQuery = {},
13631358
params: Params | null = null
@@ -1974,18 +1969,27 @@ export default class Server {
19741969
if (isNoFallbackError && bubbleNoFallback) {
19751970
throw err
19761971
}
1972+
19771973
if (err && err.code === 'DECODE_FAILED') {
1978-
this.logError(err)
19791974
res.statusCode = 400
19801975
return await this.renderErrorToHTML(err, req, res, pathname, query)
19811976
}
19821977
res.statusCode = 500
1983-
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1978+
const isWrappedError = err instanceof WrappedBuildError
1979+
const html = await this.renderErrorToHTML(
1980+
isWrappedError ? err.innerError : err,
1981+
req,
1982+
res,
1983+
pathname,
1984+
query
1985+
)
19841986

1985-
if (this.minimalMode) {
1986-
throw err
1987+
if (!isWrappedError) {
1988+
if (this.minimalMode) {
1989+
throw err
1990+
}
1991+
this.logError(err)
19871992
}
1988-
this.logError(err)
19891993
return html
19901994
}
19911995
res.statusCode = 404
@@ -2027,12 +2031,19 @@ export default class Server {
20272031
})
20282032

20292033
public async renderErrorToHTML(
2030-
err: Error | null,
2034+
_err: Error | null,
20312035
req: IncomingMessage,
20322036
res: ServerResponse,
20332037
_pathname: string,
20342038
query: ParsedUrlQuery = {}
20352039
) {
2040+
let err = _err
2041+
if (this.renderOpts.dev && !err && res.statusCode === 500) {
2042+
err = new Error(
2043+
'An undefined error was thrown sometime during render... ' +
2044+
'See https://nextjs.org/docs/messages/threw-undefined'
2045+
)
2046+
}
20362047
let html: string | null
20372048
try {
20382049
let result: null | FindComponentsResult = null
@@ -2083,24 +2094,29 @@ export default class Server {
20832094
throw maybeFallbackError
20842095
}
20852096
} catch (renderToHtmlError) {
2086-
console.error(renderToHtmlError)
2097+
const isWrappedError = renderToHtmlError instanceof WrappedBuildError
2098+
if (!isWrappedError) {
2099+
this.logError(renderToHtmlError)
2100+
}
20872101
res.statusCode = 500
2102+
const fallbackComponents = await this.getFallbackErrorComponents()
20882103

2089-
if (this.renderOpts.dev) {
2090-
await ((this as any).hotReloader as HotReloader).buildFallbackError()
2091-
2092-
const fallbackResult = await loadDefaultErrorComponents(this.distDir)
2104+
if (fallbackComponents) {
20932105
return this.renderToHTMLWithComponents(
20942106
req,
20952107
res,
20962108
'/_error',
20972109
{
20982110
query,
2099-
components: fallbackResult,
2111+
components: fallbackComponents,
21002112
},
21012113
{
21022114
...this.renderOpts,
2103-
err,
2115+
// We render `renderToHtmlError` here because `err` is
2116+
// already captured in the stacktrace.
2117+
err: isWrappedError
2118+
? renderToHtmlError.innerError
2119+
: renderToHtmlError,
21042120
}
21052121
)
21062122
}
@@ -2109,6 +2125,11 @@ export default class Server {
21092125
return html
21102126
}
21112127

2128+
protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
2129+
// The development server will provide an implementation for this
2130+
return null
2131+
}
2132+
21122133
public async render404(
21132134
req: IncomingMessage,
21142135
res: ServerResponse,
@@ -2269,3 +2290,14 @@ function prepareServerlessUrl(
22692290
}
22702291

22712292
class NoFallbackError extends Error {}
2293+
2294+
// Internal wrapper around build errors at development
2295+
// time, to prevent us from propagating or logging them
2296+
export class WrappedBuildError extends Error {
2297+
innerError: Error
2298+
2299+
constructor(innerError: Error) {
2300+
super()
2301+
this.innerError = innerError
2302+
}
2303+
}

packages/next/server/next-dev-server.ts

Lines changed: 27 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import React from 'react'
99
import { UrlWithParsedQuery } from 'url'
1010
import Watchpack from 'watchpack'
1111
import { ampValidation } from '../build/output/index'
12-
import * as Log from '../build/output/log'
1312
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../lib/constants'
1413
import { fileExists } from '../lib/file-exists'
1514
import { findPagesDir } from '../lib/find-pages-dir'
@@ -19,7 +18,6 @@ import {
1918
PHASE_DEVELOPMENT_SERVER,
2019
CLIENT_STATIC_FILES_PATH,
2120
DEV_CLIENT_PAGES_MANIFEST,
22-
STATIC_STATUS_PAGES,
2321
} from '../next-server/lib/constants'
2422
import {
2523
getRouteMatcher,
@@ -28,7 +26,11 @@ import {
2826
isDynamicRoute,
2927
} from '../next-server/lib/router/utils'
3028
import { __ApiPreviewProps } from '../next-server/server/api-utils'
31-
import Server, { ServerConstructor } from '../next-server/server/next-server'
29+
import Server, {
30+
WrappedBuildError,
31+
ServerConstructor,
32+
FindComponentsResult,
33+
} from '../next-server/server/next-server'
3234
import { normalizePagePath } from '../next-server/server/normalize-page-path'
3335
import Router, { Params, route } from '../next-server/server/router'
3436
import { eventCliSession } from '../telemetry/events'
@@ -39,6 +41,11 @@ import { findPageFile } from './lib/find-page-file'
3941
import { getNodeOptionsWithoutInspect } from './lib/utils'
4042
import { withCoalescedInvoke } from '../lib/coalesced-function'
4143
import { NextConfig } from '../next-server/server/config'
44+
import { ParsedUrlQuery } from 'querystring'
45+
import {
46+
LoadComponentsReturnType,
47+
loadDefaultErrorComponents,
48+
} from '../next-server/server/load-components'
4249

4350
if (typeof React.Suspense === 'undefined') {
4451
throw new Error(
@@ -600,95 +607,34 @@ export default class DevServer extends Server {
600607
return this.hotReloader!.ensurePage(pathname)
601608
}
602609

603-
async renderToHTML(
604-
req: IncomingMessage,
605-
res: ServerResponse,
610+
protected async findPageComponents(
606611
pathname: string,
607-
query: { [key: string]: string }
608-
): Promise<string | null> {
612+
query: ParsedUrlQuery = {},
613+
params: Params | null = null
614+
): Promise<FindComponentsResult | null> {
609615
await this.devReady
610616
const compilationErr = await this.getCompilationError(pathname)
611617
if (compilationErr) {
612-
res.statusCode = 500
613-
return this.renderErrorToHTML(compilationErr, req, res, pathname, query)
618+
// Wrap build errors so that they don't get logged again
619+
throw new WrappedBuildError(compilationErr)
614620
}
615-
616-
// In dev mode we use on demand entries to compile the page before rendering
617621
try {
618-
await this.hotReloader!.ensurePage(pathname).catch(async (err: Error) => {
619-
if ((err as any).code !== 'ENOENT') {
620-
throw err
621-
}
622-
623-
for (const dynamicRoute of this.dynamicRoutes || []) {
624-
const params = dynamicRoute.match(pathname)
625-
if (!params) {
626-
continue
627-
}
628-
629-
return this.hotReloader!.ensurePage(dynamicRoute.page)
630-
}
631-
throw err
632-
})
622+
await this.hotReloader!.ensurePage(pathname)
623+
return super.findPageComponents(pathname, query, params)
633624
} catch (err) {
634-
if (err.code === 'ENOENT') {
635-
try {
636-
await this.hotReloader!.ensurePage('/404')
637-
} catch (hotReloaderError) {
638-
if (hotReloaderError.code !== 'ENOENT') {
639-
throw hotReloaderError
640-
}
641-
}
642-
643-
res.statusCode = 404
644-
return this.renderErrorToHTML(null, req, res, pathname, query)
625+
if ((err as any).code !== 'ENOENT') {
626+
throw err
645627
}
646-
if (!this.quiet) console.error(err)
628+
return null
647629
}
648-
const html = await super.renderToHTML(req, res, pathname, query)
649-
return html
650630
}
651631

652-
async renderErrorToHTML(
653-
err: Error | null,
654-
req: IncomingMessage,
655-
res: ServerResponse,
656-
pathname: string,
657-
query: { [key: string]: string }
658-
): Promise<string | null> {
659-
await this.devReady
660-
if (res.statusCode === 404 && (await this.hasPage('/404'))) {
661-
await this.hotReloader!.ensurePage('/404')
662-
} else if (
663-
STATIC_STATUS_PAGES.includes(`/${res.statusCode}`) &&
664-
(await this.hasPage(`/${res.statusCode}`))
665-
) {
666-
await this.hotReloader!.ensurePage(`/${res.statusCode}`)
667-
} else {
668-
await this.hotReloader!.ensurePage('/_error')
669-
}
670-
671-
const compilationErr = await this.getCompilationError(pathname)
672-
if (compilationErr) {
673-
res.statusCode = 500
674-
return super.renderErrorToHTML(compilationErr, req, res, pathname, query)
675-
}
676-
677-
if (!err && res.statusCode === 500) {
678-
err = new Error(
679-
'An undefined error was thrown sometime during render... ' +
680-
'See https://nextjs.org/docs/messages/threw-undefined'
681-
)
682-
}
683-
684-
try {
685-
const out = await super.renderErrorToHTML(err, req, res, pathname, query)
686-
return out
687-
} catch (err2) {
688-
if (!this.quiet) Log.error(err2)
689-
res.statusCode = 500
690-
return super.renderErrorToHTML(err2, req, res, pathname, query)
691-
}
632+
protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
633+
await this.hotReloader!.buildFallbackError()
634+
// Build the error page to ensure the fallback is built too.
635+
// TODO: See if this can be moved into hotReloader or removed.
636+
await this.hotReloader!.ensurePage('/_error')
637+
return await loadDefaultErrorComponents(this.distDir)
692638
}
693639

694640
sendHTML(

0 commit comments

Comments
 (0)