mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-19 07:51:18 +00:00
feat(nuxt): align nuxt error handling with nitro (#31230)
Co-authored-by: Daniel Philip Johnson <daniel-philip-johnson@outlook.com> Co-authored-by: Saeid Zareie <saeid.za98@gmail.com>
This commit is contained in:
parent
72e8e2065c
commit
7a92b766f5
@ -21,7 +21,7 @@ export default createConfigForNuxt({
|
||||
'packages/schema/schema/**',
|
||||
'packages/nuxt/src/app/components/welcome.vue',
|
||||
'packages/nuxt/src/app/components/error-*.vue',
|
||||
'packages/nuxt/src/core/runtime/nitro/handlers/error-*',
|
||||
'packages/nuxt/src/core/runtime/nitro/templates/error-*',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
4
packages/nuxt/.gitignore
vendored
4
packages/nuxt/.gitignore
vendored
@ -2,5 +2,5 @@ src/app/components/error-404.vue
|
||||
src/app/components/error-500.vue
|
||||
src/app/components/error-dev.vue
|
||||
src/app/components/welcome.vue
|
||||
src/core/runtime/nitro/handlers/error-500.ts
|
||||
src/core/runtime/nitro/handlers/error-dev.ts
|
||||
src/core/runtime/nitro/templates/error-500.ts
|
||||
src/core/runtime/nitro/templates/error-dev.ts
|
||||
|
@ -14,8 +14,9 @@ export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
|
||||
/** @since 3.0.0 */
|
||||
export const useError = (): Ref<NuxtPayload['error']> => toRef(useNuxtApp().payload, 'error')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface NuxtError<DataT = unknown> extends H3Error<DataT> {}
|
||||
export interface NuxtError<DataT = unknown> extends H3Error<DataT> {
|
||||
error?: true
|
||||
}
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export const showError = <DataT = unknown>(
|
||||
|
@ -1,53 +1,40 @@
|
||||
import { joinURL, withQuery } from 'ufo'
|
||||
import type { NitroErrorHandler } from 'nitropack'
|
||||
import type { H3Error } from 'h3'
|
||||
import { getRequestHeaders, send, setResponseHeader, setResponseStatus } from 'h3'
|
||||
|
||||
import type { NuxtPayload } from 'nuxt/app'
|
||||
import { getRequestHeaders, send, setResponseHeader, setResponseHeaders, setResponseStatus } from 'h3'
|
||||
|
||||
import { isJsonRequest } from '../utils/error'
|
||||
import { useRuntimeConfig } from '#internal/nitro'
|
||||
import { useNitroApp } from '#internal/nitro/app'
|
||||
import { isJsonRequest, normalizeError } from '#internal/nitro/utils'
|
||||
import type { NuxtPayload } from '#app/nuxt'
|
||||
|
||||
export default <NitroErrorHandler> async function errorhandler (error: H3Error, event) {
|
||||
// Parse and normalize error
|
||||
const { stack, statusCode, statusMessage, message } = normalizeError(error)
|
||||
|
||||
// Create an error object
|
||||
const errorObject = {
|
||||
url: event.path,
|
||||
statusCode,
|
||||
statusMessage,
|
||||
message,
|
||||
stack: import.meta.dev && statusCode !== 404
|
||||
? `<pre>${stack.map(i => `<span class="stack${i.internal ? ' internal' : ''}">${i.text}</span>`).join('\n')}</pre>`
|
||||
: '',
|
||||
// TODO: check and validate error.data for serialisation into query
|
||||
data: error.data as any,
|
||||
} satisfies Partial<NuxtPayload['error']> & { url: string }
|
||||
|
||||
// Console output
|
||||
if (error.unhandled || error.fatal) {
|
||||
const tags = [
|
||||
'[nuxt]',
|
||||
'[request error]',
|
||||
error.unhandled && '[unhandled]',
|
||||
error.fatal && '[fatal]',
|
||||
Number(errorObject.statusCode) !== 200 && `[${errorObject.statusCode}]`,
|
||||
].filter(Boolean).join(' ')
|
||||
console.error(tags, (error.message || error.toString() || 'internal server error') + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
|
||||
}
|
||||
|
||||
if (event.handled) { return }
|
||||
|
||||
// Set response code and message
|
||||
setResponseStatus(event, (errorObject.statusCode !== 200 && errorObject.statusCode) as any as number || 500, errorObject.statusMessage)
|
||||
|
||||
// JSON response
|
||||
export default <NitroErrorHandler> async function errorhandler (error, event, { defaultHandler }) {
|
||||
if (isJsonRequest(event)) {
|
||||
setResponseHeader(event, 'Content-Type', 'application/json')
|
||||
return send(event, JSON.stringify(errorObject))
|
||||
// let Nitro handle JSON errors
|
||||
return
|
||||
}
|
||||
// invoke default Nitro error handler (which will log appropriately if required)
|
||||
const defaultRes = await defaultHandler(error, event, { json: true })
|
||||
|
||||
// let Nitro handle redirect if appropriate
|
||||
const statusCode = error.statusCode || 500
|
||||
if (statusCode === 404 && defaultRes.status === 302) {
|
||||
setResponseHeaders(event, defaultRes.headers)
|
||||
setResponseStatus(event, defaultRes.status, defaultRes.statusText)
|
||||
return send(event, JSON.stringify(defaultRes.body, null, 2))
|
||||
}
|
||||
|
||||
if (import.meta.dev && typeof defaultRes.body !== 'string' && Array.isArray(defaultRes.body.stack)) {
|
||||
// normalize to string format expected by nuxt `error.vue`
|
||||
defaultRes.body.stack = defaultRes.body.stack.join('\n')
|
||||
}
|
||||
|
||||
const errorObject = defaultRes.body as Pick<NonNullable<NuxtPayload['error']>, 'error' | 'statusCode' | 'statusMessage' | 'message' | 'stack'> & { url: string, data: any }
|
||||
errorObject.message ||= 'Server Error'
|
||||
|
||||
delete defaultRes.headers['content-type'] // this would be set to application/json
|
||||
delete defaultRes.headers['content-security-policy'] // this would disable JS execution in the error page
|
||||
|
||||
setResponseHeaders(event, defaultRes.headers)
|
||||
|
||||
// Access request headers
|
||||
const reqHeaders = getRequestHeaders(event)
|
||||
@ -66,25 +53,24 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
|
||||
},
|
||||
).catch(() => null)
|
||||
|
||||
if (event.handled) { return }
|
||||
|
||||
// Fallback to static rendered error page
|
||||
if (!res) {
|
||||
const { template } = import.meta.dev ? await import('./error-dev') : await import('./error-500')
|
||||
const { template } = import.meta.dev ? await import('../templates/error-dev') : await import('../templates/error-500')
|
||||
if (import.meta.dev) {
|
||||
// TODO: Support `message` in template
|
||||
(errorObject as any).description = errorObject.message
|
||||
}
|
||||
if (event.handled) { return }
|
||||
setResponseHeader(event, 'Content-Type', 'text/html;charset=UTF-8')
|
||||
return send(event, template(errorObject))
|
||||
}
|
||||
|
||||
const html = await res.text()
|
||||
if (event.handled) { return }
|
||||
|
||||
for (const [header, value] of res.headers.entries()) {
|
||||
setResponseHeader(event, header, value)
|
||||
}
|
||||
setResponseStatus(event, res.status && res.status !== 200 ? res.status : undefined, res.statusText)
|
||||
setResponseStatus(event, res.status && res.status !== 200 ? res.status : defaultRes.status, res.statusText || defaultRes.statusText)
|
||||
|
||||
return send(event, html)
|
||||
}
|
||||
|
27
packages/nuxt/src/core/runtime/nitro/utils/error.ts
Normal file
27
packages/nuxt/src/core/runtime/nitro/utils/error.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { type H3Event, getRequestHeader } from 'h3'
|
||||
|
||||
/**
|
||||
* Nitro internal functions extracted from https://github.com/nitrojs/nitro/blob/main/src/runtime/internal/utils.ts
|
||||
*/
|
||||
|
||||
export function isJsonRequest (event: H3Event) {
|
||||
// If the client specifically requests HTML, then avoid classifying as JSON.
|
||||
if (hasReqHeader(event, 'accept', 'text/html')) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
hasReqHeader(event, 'accept', 'application/json') ||
|
||||
hasReqHeader(event, 'user-agent', 'curl/') ||
|
||||
hasReqHeader(event, 'user-agent', 'httpie/') ||
|
||||
hasReqHeader(event, 'sec-fetch-mode', 'cors') ||
|
||||
event.path.startsWith('/api/') ||
|
||||
event.path.endsWith('.json')
|
||||
)
|
||||
}
|
||||
|
||||
export function hasReqHeader (event: H3Event, name: string, includes: string) {
|
||||
const value = getRequestHeader(event, name)
|
||||
return (
|
||||
value && typeof value === 'string' && value.toLowerCase().includes(includes)
|
||||
)
|
||||
}
|
@ -1166,11 +1166,14 @@ describe('errors', () => {
|
||||
expect(res.statusText).toBe('This is a custom error')
|
||||
const error = await res.json()
|
||||
delete error.stack
|
||||
const url = new URL(error.url)
|
||||
url.host = 'localhost:3000'
|
||||
error.url = url.toString()
|
||||
expect(error).toMatchObject({
|
||||
message: 'This is a custom error',
|
||||
message: isDev() ? 'This is a custom error' : 'Server Error',
|
||||
statusCode: 422,
|
||||
statusMessage: 'This is a custom error',
|
||||
url: '/error',
|
||||
url: 'http://localhost:3000/error',
|
||||
})
|
||||
})
|
||||
|
||||
@ -1189,12 +1192,17 @@ describe('errors', () => {
|
||||
expect(res.status).toBe(404)
|
||||
const error = await res.json()
|
||||
delete error.stack
|
||||
const url = new URL(error.url)
|
||||
url.host = 'localhost:3000'
|
||||
error.url = url.toString()
|
||||
|
||||
expect(error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": true,
|
||||
"message": "Page Not Found: /__nuxt_error",
|
||||
"statusCode": 404,
|
||||
"statusMessage": "Page Not Found: /__nuxt_error",
|
||||
"url": "/__nuxt_error",
|
||||
"url": "http://localhost:3000/__nuxt_error",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
@ -37,7 +37,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"193k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"192k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1384k"`)
|
||||
@ -74,7 +74,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output-inline/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"544k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"543k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"77.8k"`)
|
||||
|
Loading…
Reference in New Issue
Block a user