diff --git a/packages/server/src/middleware/error.js b/packages/server/src/middleware/error.js index 01cf3a2b88..2361ef12de 100644 --- a/packages/server/src/middleware/error.js +++ b/packages/server/src/middleware/error.js @@ -6,19 +6,21 @@ import Youch from '@nuxtjs/youch' export default ({ resources, options }) => function errorMiddleware(err, req, res, next) { // 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 - if (err.statusCode !== 404) { - consola.error(err) + const error = { + statusCode: err.statusCode || 500, + 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') => { // Set Headers - res.statusCode = err.statusCode - res.statusMessage = err.name + res.statusCode = error.statusCode + res.statusMessage = error.name res.setHeader('Content-Type', type + '; charset=utf-8') res.setHeader('Content-Length', Buffer.byteLength(content)) 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 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 const json = { - status: err.statusCode, - message: err.message, - name: err.name + status: error.statusCode, + message: error.message, + name: error.name } if (isJson) { sendResponse(JSON.stringify(json, undefined, 2), 'text/json') @@ -53,7 +59,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re // Show stack trace const youch = new Youch( - err, + errorFull, req, readSourceFactory({ srcDir: options.srcDir, diff --git a/packages/vue-app/template/client.js b/packages/vue-app/template/client.js index 1b4d5bb3e4..21196fac77 100644 --- a/packages/vue-app/template/client.js +++ b/packages/vue-app/template/client.js @@ -36,11 +36,6 @@ Object.assign(Vue.config, <%= serialize(vue.config) %>)<%= isTest ? '// eslint-d if (!Vue.config.$nuxt) { const defaultErrorHandler = Vue.config.errorHandler 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 let handled = null if (typeof defaultErrorHandler === 'function') { @@ -53,10 +48,10 @@ if (!Vue.config.$nuxt) { if (vm && vm.$root) { const nuxtApp = Object.keys(Vue.config.$nuxt) .find(nuxtInstance => vm.$root[nuxtInstance]) - + // Show Nuxt Error Page 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') { console.error(err) } else { - console.error(err.message || nuxtError.message) + console.error(err.message || err) } } Vue.config.$nuxt = {} @@ -150,9 +145,7 @@ async function loadAsyncComponents(to, from, next) { // Call next() next() } catch (err) { - const error = err || {} - const statusCode = (error.statusCode || error.status || (error.response && error.response.status) || 500) - this.error({ statusCode, message: error.message }) + this.error(err) this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error) next(false) } @@ -414,8 +407,6 @@ async function render(to, from, next) { return this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error) } _lastPaths = [] - const errorResponseStatus = (error.response && error.response.status) - error.statusCode = error.statusCode || error.status || errorResponseStatus || 500 globalHandleError(error) diff --git a/packages/vue-app/template/index.js b/packages/vue-app/template/index.js index 24648c1349..36aac1757b 100644 --- a/packages/vue-app/template/index.js +++ b/packages/vue-app/template/index.js @@ -7,7 +7,7 @@ import NuxtLink from './components/nuxt-link.js' import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' import Nuxt from './components/nuxt.js' import App from '<%= appPath %>' -import { setContext, getLocation, getRouteData } from './utils' +import { setContext, getLocation, getRouteData, normalizeError } from './utils' <% if (store) { %>import { createStore } from './store.js'<% } %> /* Plugins */ @@ -90,7 +90,7 @@ async function createApp(ssrContext) { error(err) { err = err || null 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 nuxt.dateErr = Date.now() nuxt.err = err diff --git a/packages/vue-app/template/utils.js b/packages/vue-app/template/utils.js index 4d1d6ff629..93b9783ae5 100644 --- a/packages/vue-app/template/utils.js +++ b/packages/vue-app/template/utils.js @@ -273,6 +273,23 @@ export function getQueryDiff(toQuery, fromQuery) { 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. * @@ -548,3 +565,4 @@ function formatQuery(query) { return key + '=' + val }).filter(Boolean).join('&') } + diff --git a/test/e2e/basic.browser.test.js b/test/e2e/basic.browser.test.js index c215a4f6fa..a62f553b28 100644 --- a/test/e2e/basic.browser.test.js +++ b/test/e2e/basic.browser.test.js @@ -158,7 +158,7 @@ describe('basic browser', () => { test('/error', async () => { 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') }) @@ -166,7 +166,7 @@ describe('basic browser', () => { await page.nuxt.navigate('/error2') 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 () => { diff --git a/test/fixtures/basic/pages/error-object.vue b/test/fixtures/basic/pages/error-object.vue new file mode 100644 index 0000000000..28d30fbb18 --- /dev/null +++ b/test/fixtures/basic/pages/error-object.vue @@ -0,0 +1,8 @@ + diff --git a/test/fixtures/basic/pages/error-string.vue b/test/fixtures/basic/pages/error-string.vue new file mode 100644 index 0000000000..4b6c6d5256 --- /dev/null +++ b/test/fixtures/basic/pages/error-string.vue @@ -0,0 +1,8 @@ + diff --git a/test/fixtures/basic/pages/error.vue b/test/fixtures/basic/pages/error.vue index 2b2308b386..d6975dcc52 100644 --- a/test/fixtures/basic/pages/error.vue +++ b/test/fixtures/basic/pages/error.vue @@ -4,7 +4,7 @@ diff --git a/test/fixtures/spa/pages/error-handler-string.vue b/test/fixtures/spa/pages/error-handler-string.vue new file mode 100644 index 0000000000..4b6c6d5256 --- /dev/null +++ b/test/fixtures/spa/pages/error-handler-string.vue @@ -0,0 +1,8 @@ + diff --git a/test/fixtures/spa/plugins/error.js b/test/fixtures/spa/plugins/error.js index 27bb917894..5c6de6b278 100644 --- a/test/fixtures/spa/plugins/error.js +++ b/test/fixtures/spa/plugins/error.js @@ -1,5 +1,5 @@ import Vue from 'vue' 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}`)) } diff --git a/test/unit/basic.ssr.test.js b/test/unit/basic.ssr.test.js index 8718601efa..665ef19fbc 100644 --- a/test/unit/basic.ssr.test.js +++ b/test/unit/basic.ssr.test.js @@ -174,6 +174,26 @@ describe('basic ssr', () => { .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 () => { await expect(rp(url('/error'))).rejects.toMatchObject({ statusCode: 500 @@ -201,7 +221,7 @@ describe('basic ssr', () => { const { html, error } = await nuxt.server.renderRoute('/error2') expect(html).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 () => { diff --git a/test/unit/spa.test.js b/test/unit/spa.test.js index 01d60ad17d..e882be4392 100644 --- a/test/unit/spa.test.js +++ b/test/unit/spa.test.js @@ -41,13 +41,21 @@ describe('spa', () => { }) test('/error-handler', async () => { - await renderRoute('/error-handler') const { html } = await renderRoute('/error-handler') 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 () => { - await renderRoute('/error-handler-async') const { html } = await renderRoute('/error-handler-async') expect(html).toMatch('error handler triggered: asyncData error!') })