mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Handle runtime errors (#268)
* display runtime errors by error-debug * server: fix status * render Error component on client error * server: render runtime errors of error template * server: handle errors of error template on render404 * server: add a comment * server: refactor renderJSON * recover from runtime errors * _error: check if xhr exists * _error: improve client error * _error: improve error message
This commit is contained in:
parent
a14cc66720
commit
c7ba914f52
|
@ -188,13 +188,17 @@ import React from 'react'
|
|||
|
||||
export default class Error extends React.Component {
|
||||
static getInitialProps ({ res, xhr }) {
|
||||
const statusCode = res ? res.statusCode : xhr.status
|
||||
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<p>An error { this.props.statusCode } occurred</p>
|
||||
<p>{
|
||||
this.props.statusCode
|
||||
? `An error ${this.props.statusCode} occurred on server`
|
||||
: 'An error occurred on client'
|
||||
]</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,17 @@ import App from '../lib/app'
|
|||
import evalScript from '../lib/eval-script'
|
||||
|
||||
const {
|
||||
__NEXT_DATA__: { component, props, ids, err }
|
||||
__NEXT_DATA__: { component, errorComponent, props, ids, err }
|
||||
} = window
|
||||
|
||||
const Component = evalScript(component).default
|
||||
const ErrorComponent = evalScript(errorComponent).default
|
||||
|
||||
export const router = new Router(window.location.href, { Component, ctx: { err } })
|
||||
export const router = new Router(window.location.href, {
|
||||
Component,
|
||||
ErrorComponent,
|
||||
ctx: { err }
|
||||
})
|
||||
|
||||
const headManager = new HeadManager()
|
||||
const container = document.getElementById('__next')
|
||||
|
|
|
@ -16,6 +16,13 @@ const handlers = {
|
|||
}
|
||||
|
||||
next.router.reload(route)
|
||||
},
|
||||
change (route) {
|
||||
const { Component } = next.router.components[route] || {}
|
||||
if (Component && Component.__route === '/_error-debug') {
|
||||
// reload to recover from runtime errors
|
||||
next.router.reload(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,16 @@ import evalScript from './eval-script'
|
|||
import shallowEquals from './shallow-equals'
|
||||
|
||||
export default class Router {
|
||||
constructor (url, initialData) {
|
||||
constructor (url, { Component, ErrorComponent, ctx } = {}) {
|
||||
const parsed = parse(url, true)
|
||||
|
||||
// represents the current component key
|
||||
this.route = toRoute(parsed.pathname)
|
||||
|
||||
// set up the component cache (by route keys)
|
||||
this.components = { [this.route]: initialData }
|
||||
this.components = { [this.route]: { Component, ctx } }
|
||||
|
||||
this.ErrorComponent = ErrorComponent
|
||||
this.pathname = parsed.pathname
|
||||
this.query = parsed.query
|
||||
this.subscriptions = new Set()
|
||||
|
@ -38,13 +39,19 @@ export default class Router {
|
|||
this.route = route
|
||||
this.set(getURL(), { ...data, props })
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(async (err) => {
|
||||
if (err.cancelled) return
|
||||
|
||||
// the only way we can appropriately handle
|
||||
// this failure is deferring to the browser
|
||||
// since the URL has already changed
|
||||
window.location.reload()
|
||||
const data = { Component: this.ErrorComponent, ctx: { err } }
|
||||
const ctx = { ...data.ctx, pathname, query }
|
||||
const props = await this.getInitialProps(data.Component, ctx)
|
||||
|
||||
this.route = route
|
||||
this.set(getURL(), { ...data, props })
|
||||
console.error(err)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -67,16 +74,25 @@ export default class Router {
|
|||
|
||||
let data
|
||||
let props
|
||||
let _err
|
||||
try {
|
||||
data = await this.fetchComponent(route)
|
||||
const ctx = { ...data.ctx, pathname, query }
|
||||
props = await this.getInitialProps(data.Component, ctx)
|
||||
} catch (err) {
|
||||
if (err.cancelled) return false
|
||||
throw err
|
||||
|
||||
data = { Component: this.ErrorComponent, ctx: { err } }
|
||||
const ctx = { ...data.ctx, pathname, query }
|
||||
props = await this.getInitialProps(data.Component, ctx)
|
||||
|
||||
_err = err
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
this.notify({ ...data, props })
|
||||
|
||||
if (_err) throw _err
|
||||
}
|
||||
|
||||
back () {
|
||||
|
@ -100,13 +116,20 @@ export default class Router {
|
|||
|
||||
let data
|
||||
let props
|
||||
let _err
|
||||
try {
|
||||
data = await this.fetchComponent(route)
|
||||
const ctx = { ...data.ctx, pathname, query }
|
||||
props = await this.getInitialProps(data.Component, ctx)
|
||||
} catch (err) {
|
||||
if (err.cancelled) return false
|
||||
throw err
|
||||
|
||||
data = { Component: this.ErrorComponent, ctx: { err } }
|
||||
const ctx = { ...data.ctx, pathname, query }
|
||||
props = await this.getInitialProps(data.Component, ctx)
|
||||
|
||||
_err = err
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (getURL() !== url) {
|
||||
|
@ -115,6 +138,9 @@ export default class Router {
|
|||
|
||||
this.route = route
|
||||
this.set(url, { ...data, props })
|
||||
|
||||
if (_err) throw _err
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@ import style from 'next/css'
|
|||
|
||||
export default class ErrorDebug extends React.Component {
|
||||
static getInitialProps ({ err }) {
|
||||
const { message, module } = err
|
||||
return { message, path: module.rawRequest }
|
||||
const { name, message, stack, module } = err
|
||||
return { name, message, stack, path: module ? module.rawRequest : null }
|
||||
}
|
||||
|
||||
render () {
|
||||
const { message, path } = this.props
|
||||
const { name, message, stack, path } = this.props
|
||||
|
||||
return <div className={styles.errorDebug}>
|
||||
<Head>
|
||||
|
@ -21,8 +21,12 @@ export default class ErrorDebug extends React.Component {
|
|||
}
|
||||
`}} />
|
||||
</Head>
|
||||
<div className={styles.heading}>Error in {path}</div>
|
||||
<pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
|
||||
{path ? <div className={styles.heading}>Error in {path}</div> : null}
|
||||
{
|
||||
name === 'ModuleBuildError'
|
||||
? <pre className={styles.message} dangerouslySetInnerHTML={{ __html: ansiHTML(encodeHtml(message)) }} />
|
||||
: <pre className={styles.message}>{stack}</pre>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import React from 'react'
|
||||
import style, { merge } from 'next/css'
|
||||
import style from 'next/css'
|
||||
|
||||
export default class Error extends React.Component {
|
||||
static getInitialProps ({ res, xhr }) {
|
||||
const statusCode = res ? res.statusCode : xhr.status
|
||||
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusCode } = this.props
|
||||
const title = statusCode === 404 ? 'This page could not be found' : 'Internal Server Error'
|
||||
const title = statusCode === 404
|
||||
? 'This page could not be found'
|
||||
: (statusCode ? 'Internal Server Error' : 'An unexpected error has occurred')
|
||||
|
||||
return <div className={merge(styles.error, styles['error_' + statusCode])}>
|
||||
return <div className={styles.error}>
|
||||
<div className={styles.text}>
|
||||
<h1 className={styles.h1}>{statusCode}</h1>
|
||||
{statusCode ? <h1 className={styles.h1}>{statusCode}</h1> : null}
|
||||
<div className={styles.desc}>
|
||||
<h2 className={styles.h2}>{title}.</h2>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default class HotReloader {
|
|||
this.prevAssets = null
|
||||
this.prevChunkNames = null
|
||||
this.prevFailedChunkNames = null
|
||||
this.prevChunkHashes = null
|
||||
}
|
||||
|
||||
async run (req, res) {
|
||||
|
@ -66,6 +67,8 @@ export default class HotReloader {
|
|||
.reduce((a, b) => a.concat(b), [])
|
||||
.map((c) => c.name))
|
||||
|
||||
const chunkHashes = new Map(compilation.chunks.map((c) => [c.name, c.hash]))
|
||||
|
||||
if (this.initialized) {
|
||||
// detect chunks which have to be replaced with a new template
|
||||
// e.g, pages/index.js <-> pages/_error.js
|
||||
|
@ -83,6 +86,16 @@ export default class HotReloader {
|
|||
const route = toRoute(relative(rootDir, n))
|
||||
this.send('reload', route)
|
||||
}
|
||||
|
||||
for (const [n, hash] of chunkHashes) {
|
||||
if (!this.prevChunkHashes.has(n)) continue
|
||||
if (this.prevChunkHashes.get(n) === hash) continue
|
||||
|
||||
const route = toRoute(relative(rootDir, n))
|
||||
|
||||
// notify change to recover from runtime errors
|
||||
this.send('change', route)
|
||||
}
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
|
@ -90,6 +103,7 @@ export default class HotReloader {
|
|||
this.compilationErrors = null
|
||||
this.prevChunkNames = chunkNames
|
||||
this.prevFailedChunkNames = failedChunkNames
|
||||
this.prevChunkHashes = chunkHashes
|
||||
})
|
||||
|
||||
this.webpackDevMiddleware = webpackDevMiddleware(compiler, {
|
||||
|
|
138
server/index.js
138
server/index.js
|
@ -18,7 +18,7 @@ export default class Server {
|
|||
this.run(req, res)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
res.status(500)
|
||||
res.statusCode = 500
|
||||
res.end('error')
|
||||
})
|
||||
})
|
||||
|
@ -73,96 +73,112 @@ export default class Server {
|
|||
}
|
||||
|
||||
async render (req, res) {
|
||||
const { dir, dev } = this
|
||||
const { pathname, query } = parse(req.url, true)
|
||||
const ctx = { req, res, pathname, query }
|
||||
const opts = { dir, dev }
|
||||
|
||||
let html
|
||||
const compilationErr = this.getCompilationError(req.url)
|
||||
if (compilationErr) {
|
||||
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.doRender(res, 200, req.url, ctx)
|
||||
} catch (err) {
|
||||
const compilationErr2 = this.getCompilationError('/_error')
|
||||
if (compilationErr2) {
|
||||
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr2 })
|
||||
return
|
||||
}
|
||||
|
||||
if (err.code !== 'ENOENT') {
|
||||
console.error(err)
|
||||
const url = this.dev ? '/_error-debug' : '/_error'
|
||||
await this.doRender(res, 500, url, { ...ctx, err })
|
||||
return
|
||||
}
|
||||
|
||||
const err = this.getCompilationError(req.url)
|
||||
if (err) {
|
||||
res.statusCode = 500
|
||||
html = await render('/_error-debug', { ...ctx, err }, opts)
|
||||
} else {
|
||||
try {
|
||||
html = await render(req.url, ctx, opts)
|
||||
} catch (err) {
|
||||
const _err = this.getCompilationError('/_error')
|
||||
if (_err) {
|
||||
res.statusCode = 500
|
||||
html = await render('/_error-debug', { ...ctx, err: _err }, opts)
|
||||
await this.doRender(res, 404, '/_error', { ...ctx, err })
|
||||
} catch (err2) {
|
||||
if (this.dev) {
|
||||
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: err2 })
|
||||
} else {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.statusCode = 404
|
||||
} else {
|
||||
console.error(err)
|
||||
res.statusCode = 500
|
||||
}
|
||||
html = await render('/_error', { ...ctx, err }, opts)
|
||||
throw err2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async doRender (res, statusCode, url, ctx) {
|
||||
const { dir, dev } = this
|
||||
|
||||
// need to set statusCode before `render`
|
||||
// since it can be used on getInitialProps
|
||||
res.statusCode = statusCode
|
||||
|
||||
const html = await render(url, ctx, { dir, dev })
|
||||
sendHTML(res, html)
|
||||
}
|
||||
|
||||
async renderJSON (req, res) {
|
||||
const { dir } = this
|
||||
const opts = { dir }
|
||||
const compilationErr = this.getCompilationError(req.url)
|
||||
if (compilationErr) {
|
||||
await this.doRenderJSON(res, 500, '/_error-debug.json', compilationErr)
|
||||
return
|
||||
}
|
||||
|
||||
let json
|
||||
|
||||
const err = this.getCompilationError(req.url)
|
||||
if (err) {
|
||||
res.statusCode = 500
|
||||
json = await renderJSON('/_error-debug.json', opts)
|
||||
json = { ...json, err: errorToJSON(err) }
|
||||
} else {
|
||||
try {
|
||||
json = await renderJSON(req.url, opts)
|
||||
} catch (err) {
|
||||
const _err = this.getCompilationError('/_error.json')
|
||||
if (_err) {
|
||||
res.statusCode = 500
|
||||
json = await renderJSON('/_error-debug.json', opts)
|
||||
json = { ...json, err: errorToJSON(_err) }
|
||||
} else {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.statusCode = 404
|
||||
} else {
|
||||
console.error(err)
|
||||
res.statusCode = 500
|
||||
}
|
||||
json = await renderJSON('/_error.json', opts)
|
||||
}
|
||||
try {
|
||||
await this.doRenderJSON(res, 200, req.url)
|
||||
} catch (err) {
|
||||
const compilationErr2 = this.getCompilationError('/_error.json')
|
||||
if (compilationErr2) {
|
||||
await this.doRenderJSON(res, 500, '/_error-debug.json', compilationErr2)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.code === 'ENOENT') {
|
||||
await this.doRenderJSON(res, 404, '/_error.json')
|
||||
} else {
|
||||
console.error(err)
|
||||
await this.doRenderJSON(res, 500, '/_error.json')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async doRenderJSON (res, statusCode, url, err) {
|
||||
const { dir } = this
|
||||
const json = await renderJSON(url, { dir })
|
||||
if (err) {
|
||||
json.err = errorToJSON(err)
|
||||
}
|
||||
|
||||
const data = JSON.stringify(json)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(data))
|
||||
res.statusCode = statusCode
|
||||
res.end(data)
|
||||
}
|
||||
|
||||
async render404 (req, res) {
|
||||
const { dir, dev } = this
|
||||
const { pathname, query } = parse(req.url, true)
|
||||
const ctx = { req, res, pathname, query }
|
||||
const opts = { dir, dev }
|
||||
|
||||
let html
|
||||
|
||||
const err = this.getCompilationError('/_error')
|
||||
if (err) {
|
||||
res.statusCode = 500
|
||||
html = await render('/_error-debug', { ...ctx, err }, opts)
|
||||
} else {
|
||||
res.statusCode = 404
|
||||
html = await render('/_error', ctx, opts)
|
||||
const compilationErr = this.getCompilationError('/_error')
|
||||
if (compilationErr) {
|
||||
await this.doRender(res, 500, '/_error-debug', { ...ctx, err: compilationErr })
|
||||
return
|
||||
}
|
||||
|
||||
sendHTML(res, html)
|
||||
try {
|
||||
await this.doRender(res, 404, '/_error', ctx)
|
||||
} catch (err) {
|
||||
if (this.dev) {
|
||||
await this.doRender(res, 500, '/_error-debug', { ...ctx, err })
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serveStatic (req, res, path) {
|
||||
|
|
|
@ -20,8 +20,15 @@ export async function render (url, ctx = {}, {
|
|||
const mod = await requireModule(join(dir, '.next', 'dist', 'pages', path))
|
||||
const Component = mod.default || mod
|
||||
|
||||
const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
|
||||
const component = await read(join(dir, '.next', 'bundles', 'pages', path))
|
||||
const [
|
||||
props,
|
||||
component,
|
||||
errorComponent
|
||||
] = await Promise.all([
|
||||
Component.getInitialProps ? Component.getInitialProps(ctx) : {},
|
||||
read(join(dir, '.next', 'bundles', 'pages', path)),
|
||||
read(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error'))
|
||||
])
|
||||
|
||||
const { html, css, ids } = renderStatic(() => {
|
||||
const app = createElement(App, {
|
||||
|
@ -42,6 +49,7 @@ export async function render (url, ctx = {}, {
|
|||
css,
|
||||
data: {
|
||||
component,
|
||||
errorComponent,
|
||||
props,
|
||||
ids: ids,
|
||||
err: (ctx.err && dev) ? errorToJSON(ctx.err) : null
|
||||
|
|
Loading…
Reference in a new issue