fix: handle errors that are not error instances (#4321)

This commit is contained in:
Dmitry Molotkov 2018-11-14 22:17:44 +03:00 committed by Pooya Parsa
parent 846455e2f7
commit 9fbd581557
13 changed files with 109 additions and 34 deletions

View File

@ -6,19 +6,21 @@ import Youch from '@nuxtjs/youch'
export default ({ resources, options }) => function errorMiddleware(err, req, res, next) { export default ({ resources, options }) => function errorMiddleware(err, req, res, next) {
// ensure statusCode, message and name fields // ensure statusCode, message and name fields
err.statusCode = err.statusCode || 500
err.message = err.message || 'Nuxt Server Error'
err.name = !err.name || err.name === 'Error' ? 'NuxtServerError' : err.name
// We hide actual errors from end users, so show them on server logs const error = {
if (err.statusCode !== 404) { statusCode: err.statusCode || 500,
consola.error(err) message: err.message || 'Nuxt Server Error',
name: !err.name || err.name === 'Error' ? 'NuxtServerError' : err.name
} }
const errorFull = err instanceof Error ? err : typeof err === 'string'
? new Error(err) : new Error(err.message || JSON.stringify(err))
errorFull.name = error.name
errorFull.statusCode = error.statusCode
const sendResponse = (content, type = 'text/html') => { const sendResponse = (content, type = 'text/html') => {
// Set Headers // Set Headers
res.statusCode = err.statusCode res.statusCode = error.statusCode
res.statusMessage = err.name res.statusMessage = error.name
res.setHeader('Content-Type', type + '; charset=utf-8') res.setHeader('Content-Type', type + '; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(content)) res.setHeader('Content-Length', Buffer.byteLength(content))
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate') res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
@ -36,11 +38,15 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
// Use basic errors when debug mode is disabled // Use basic errors when debug mode is disabled
if (!options.debug) { if (!options.debug) {
// We hide actual errors from end users, so show them on server logs
if (err.statusCode !== 404) {
consola.error(err)
}
// Json format is compatible with Youch json responses // Json format is compatible with Youch json responses
const json = { const json = {
status: err.statusCode, status: error.statusCode,
message: err.message, message: error.message,
name: err.name name: error.name
} }
if (isJson) { if (isJson) {
sendResponse(JSON.stringify(json, undefined, 2), 'text/json') sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
@ -53,7 +59,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
// Show stack trace // Show stack trace
const youch = new Youch( const youch = new Youch(
err, errorFull,
req, req,
readSourceFactory({ readSourceFactory({
srcDir: options.srcDir, srcDir: options.srcDir,

View File

@ -36,11 +36,6 @@ Object.assign(Vue.config, <%= serialize(vue.config) %>)<%= isTest ? '// eslint-d
if (!Vue.config.$nuxt) { if (!Vue.config.$nuxt) {
const defaultErrorHandler = Vue.config.errorHandler const defaultErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (err, vm, info, ...rest) => { Vue.config.errorHandler = (err, vm, info, ...rest) => {
const nuxtError = {
statusCode: err.statusCode || err.name || 'Whoops!',
message: err.message || err.toString()
}
// Call other handler if exist // Call other handler if exist
let handled = null let handled = null
if (typeof defaultErrorHandler === 'function') { if (typeof defaultErrorHandler === 'function') {
@ -56,7 +51,7 @@ if (!Vue.config.$nuxt) {
// Show Nuxt Error Page // Show Nuxt Error Page
if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') { if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') {
vm.$root[nuxtApp].error(nuxtError) vm.$root[nuxtApp].error(err)
} }
} }
@ -68,7 +63,7 @@ if (!Vue.config.$nuxt) {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.error(err) console.error(err)
} else { } else {
console.error(err.message || nuxtError.message) console.error(err.message || err)
} }
} }
Vue.config.$nuxt = {} Vue.config.$nuxt = {}
@ -150,9 +145,7 @@ async function loadAsyncComponents(to, from, next) {
// Call next() // Call next()
next() next()
} catch (err) { } catch (err) {
const error = err || {} this.error(err)
const statusCode = (error.statusCode || error.status || (error.response && error.response.status) || 500)
this.error({ statusCode, message: error.message })
this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error) this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
next(false) next(false)
} }
@ -414,8 +407,6 @@ async function render(to, from, next) {
return this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error) return this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
} }
_lastPaths = [] _lastPaths = []
const errorResponseStatus = (error.response && error.response.status)
error.statusCode = error.statusCode || error.status || errorResponseStatus || 500
globalHandleError(error) globalHandleError(error)

View File

@ -7,7 +7,7 @@ import NuxtLink from './components/nuxt-link.js'
import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>'
import Nuxt from './components/nuxt.js' import Nuxt from './components/nuxt.js'
import App from '<%= appPath %>' import App from '<%= appPath %>'
import { setContext, getLocation, getRouteData } from './utils' import { setContext, getLocation, getRouteData, normalizeError } from './utils'
<% if (store) { %>import { createStore } from './store.js'<% } %> <% if (store) { %>import { createStore } from './store.js'<% } %>
/* Plugins */ /* Plugins */
@ -90,7 +90,7 @@ async function createApp(ssrContext) {
error(err) { error(err) {
err = err || null err = err || null
app.context._errored = !!err app.context._errored = !!err
if (typeof err === 'string') err = { statusCode: 500, message: err } err = err ? normalizeError(err) : null
const nuxt = this.nuxt || this.$options.nuxt const nuxt = this.nuxt || this.$options.nuxt
nuxt.dateErr = Date.now() nuxt.dateErr = Date.now()
nuxt.err = err nuxt.err = err

View File

@ -273,6 +273,23 @@ export function getQueryDiff(toQuery, fromQuery) {
return diff return diff
} }
export function normalizeError(err) {
let message
if (!(err.message || typeof err === 'string')) {
try {
message = JSON.stringify(err, null, 2)
} catch (e) {
message = `[${err.constructor.name}]`
}
} else {
message = err.message || err
}
return {
message: message,
statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500)
}
}
/** /**
* The main path matching regexp utility. * The main path matching regexp utility.
* *
@ -548,3 +565,4 @@ function formatQuery(query) {
return key + '=' + val return key + '=' + val
}).filter(Boolean).join('&') }).filter(Boolean).join('&')
} }

View File

@ -158,7 +158,7 @@ describe('basic browser', () => {
test('/error', async () => { test('/error', async () => {
await page.nuxt.navigate('/error') await page.nuxt.navigate('/error')
expect(await page.nuxt.errorData()).toEqual({ statusCode: 500 }) expect(await page.nuxt.errorData()).toEqual({ message: 'Error mouahahah', statusCode: 500 })
expect(await page.$text('.title')).toBe('Error mouahahah') expect(await page.$text('.title')).toBe('Error mouahahah')
}) })
@ -166,7 +166,7 @@ describe('basic browser', () => {
await page.nuxt.navigate('/error2') await page.nuxt.navigate('/error2')
expect(await page.$text('.title')).toBe('Custom error') expect(await page.$text('.title')).toBe('Custom error')
expect(await page.nuxt.errorData()).toEqual({ message: 'Custom error' }) expect(await page.nuxt.errorData()).toEqual({ message: 'Custom error', statusCode: 500 })
}) })
test('/redirect-middleware', async () => { test('/redirect-middleware', async () => {

View File

@ -0,0 +1,8 @@
<script>
export default {
fetch() {
throw { error: 'fetch error!' } // eslint-disable-line
}
}
</script>

View File

@ -0,0 +1,8 @@
<script>
export default {
fetch() {
throw 'fetch error!' // eslint-disable-line
}
}
</script>

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
asyncData({ req }) { asyncData() {
throw new Error('Error mouahahah') throw new Error('Error mouahahah')
} }
} }

View File

@ -0,0 +1,8 @@
<script>
export default {
fetch() {
throw { error: 'fetch error!' } // eslint-disable-line
}
}
</script>

View File

@ -0,0 +1,8 @@
<script>
export default {
fetch() {
throw 'fetch error!' // eslint-disable-line
}
}
</script>

View File

@ -1,5 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
Vue.config.errorHandler = function (err) { Vue.config.errorHandler = function (err) {
document.body.appendChild(document.createTextNode(`error handler triggered: ${err.message}`)) document.body.appendChild(document.createTextNode(`error handler triggered: ${err.message || err}`))
} }

View File

@ -174,6 +174,26 @@ describe('basic ssr', () => {
.rejects.toThrow('Error mouahahah') .rejects.toThrow('Error mouahahah')
}) })
test('/error-string', async () => {
let error
try {
await nuxt.server.renderRoute('/error-string', { req: {}, res: {} })
} catch (e) {
error = e
}
await expect(error).toEqual('fetch error!')
})
test('/error-object', async () => {
let error
try {
await nuxt.server.renderRoute('/error-object', { req: {}, res: {} })
} catch (e) {
error = e
}
await expect(error).toEqual({ error: 'fetch error!' })
})
test('/error status code', async () => { test('/error status code', async () => {
await expect(rp(url('/error'))).rejects.toMatchObject({ await expect(rp(url('/error'))).rejects.toMatchObject({
statusCode: 500 statusCode: 500
@ -201,7 +221,7 @@ describe('basic ssr', () => {
const { html, error } = await nuxt.server.renderRoute('/error2') const { html, error } = await nuxt.server.renderRoute('/error2')
expect(html).toContain('Custom error') expect(html).toContain('Custom error')
expect(error.message).toContain('Custom error') expect(error.message).toContain('Custom error')
expect(error.statusCode === undefined).toBe(true) expect(error.statusCode).toBe(500)
}) })
test('/error2 status code', async () => { test('/error2 status code', async () => {

View File

@ -41,13 +41,21 @@ describe('spa', () => {
}) })
test('/error-handler', async () => { test('/error-handler', async () => {
await renderRoute('/error-handler')
const { html } = await renderRoute('/error-handler') const { html } = await renderRoute('/error-handler')
expect(html).toMatch('error handler triggered: fetch error!') expect(html).toMatch('error handler triggered: fetch error!')
}) })
test('/error-handler-object', async () => {
const { html } = await renderRoute('/error-handler')
expect(html).toMatch('error handler triggered: fetch error!')
})
test('/error-handler-string', async () => {
const { html } = await renderRoute('/error-handler-string')
expect(html).toMatch('error handler triggered: fetch error!')
})
test('/error-handler-async', async () => { test('/error-handler-async', async () => {
await renderRoute('/error-handler-async')
const { html } = await renderRoute('/error-handler-async') const { html } = await renderRoute('/error-handler-async')
expect(html).toMatch('error handler triggered: asyncData error!') expect(html).toMatch('error handler triggered: asyncData error!')
}) })