Nuxt/lib/core/middleware/error.js

137 lines
4.0 KiB
JavaScript

const Youch = require('@nuxtjs/youch')
const { SourceMapConsumer } = require('source-map')
const { join, resolve } = require('path')
const { readFile } = require('fs-extra')
module.exports = 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) {
console.error(err) // eslint-disable-line no-console
}
const sendResponse = (content, type = 'text/html') => {
// Set Headers
res.statusCode = err.statusCode
res.statusMessage = err.name
res.setHeader('Content-Type', type + '; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(content))
// Send Response
res.end(content, 'utf-8')
}
// Check if request accepts JSON
const hasReqHeader = (header, includes) => req.headers[header] && req.headers[header].toLowerCase().includes(includes)
const isJson = hasReqHeader('accept', 'application/json') || hasReqHeader('user-agent', 'curl/')
// Use basic errors when debug mode is disabled
if (!this.options.debug) {
// Json format is compatible with Youch json responses
const json = {
status: err.statusCode,
message: err.message,
name: err.name
}
if (isJson) {
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
return
}
const html = this.resources.errorTemplate(json)
sendResponse(html)
return
}
// Show stack trace
const youch = new Youch(err, req, readSource.bind(this))
if (isJson) {
youch.toJSON().then(json => { sendResponse(JSON.stringify(json, undefined, 2), 'text/json') })
} else {
youch.toHTML().then(html => { sendResponse(html) })
}
}
async function readSource(frame) {
const serverBundle = this.resources.serverBundle
// Remove webpack:/// & query string from the end
const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : ''
// SourceMap Support for SSR Bundle
if (serverBundle && serverBundle.maps[frame.fileName]) {
// Initialize smc cache
if (!serverBundle.$maps) {
serverBundle.$maps = {}
}
// Read SourceMap object
const smc = serverBundle.$maps[frame.fileName] || new SourceMapConsumer(serverBundle.maps[frame.fileName])
serverBundle.$maps[frame.fileName] = smc
// Try to find original position
const { line, column, name, source } = smc.originalPositionFor({
line: frame.getLineNumber() || 0,
column: frame.getColumnNumber() || 0,
bias: SourceMapConsumer.LEAST_UPPER_BOUND
})
if (line) {
frame.lineNumber = line
}
/* istanbul ignore if */
if (column) {
frame.columnNumber = column
}
/* istanbul ignore if */
if (name) {
frame.functionName = name
}
if (source) {
frame.fileName = sanitizeName(source)
// Source detected, try to get original source code
const contents = smc.sourceContentFor(source)
if (contents) {
frame.contents = contents
}
}
}
// Return if fileName is still unknown
if (!frame.fileName) {
return
}
frame.fileName = sanitizeName(frame.fileName)
// Try to read from SSR bundle files
if (serverBundle && serverBundle.files[frame.fileName]) {
frame.contents = serverBundle.files[frame.fileName]
return
}
// Possible paths for file
const searchPath = [
this.options.rootDir,
join(this.options.buildDir, 'dist'),
this.options.srcDir,
this.options.buildDir
]
// Scan filesystem for real path
for (let pathDir of searchPath) {
let fullPath = resolve(pathDir, frame.fileName)
let source = await readFile(fullPath, 'utf-8').catch(() => null)
if (source) {
if (!frame.contents) {
frame.contents = source
}
frame.fullPath = fullPath
return
}
}
}