Environment
evlog: 2.19.1
nuxt: 4.4.8 (compatibilityVersion 5)
nitropack: 2.13.4
Summary
With evlog/nuxt installed, every SSR error (404/500) returns a raw JSON body instead of rendering the framework's error page (app/error.vue in Nuxt). A user navigating to a page that throws createError({ statusCode: 404 }) sees this in the browser:
{"url":"/user/does-not-exist","status":404,"statusCode":404,"statusText":"...","statusMessage":"...","message":"...","error":true}
instead of the custom error page. Removing evlog/nuxt immediately restores correct error-page rendering.
Root cause
src/nuxt/module.ts registers evlog's Nitro error handler by prepending it to whatever is already configured:
nuxt.hook('nitro:config', (nitroConfig) => {
const evlogHandler = resolver.resolve('../nitro/errorHandler')...
nitroConfig.errorHandler = prependNitroErrorHandler(nitroConfig.errorHandler, evlogHandler);
...
});
In a Nuxt app, Nuxt registers its own Nitro error handler (@nuxt/nitro-server/.../handlers/error) which renders error.vue. After evlog's hook runs, the resolved chain is:
errorHandler: [ "…/evlog/dist/nitro/errorHandler", "…/@nuxt/nitro-server/dist/runtime/handlers/error" ]
Nitro executes handlers in order. evlog's handler (src/nitro/errorHandler.ts) runs first and, for any non-EvlogError, unconditionally builds a JSON body and ends the Node response:
const evlogError = resolveEvlogError(error);
if (!evlogError) {
const body = buildPlainNitroErrorBody(error, url, isDev);
setResponseStatus(event, body.status);
setResponseHeader(event, "Content-Type", "application/json");
return endNodeResponse(event, JSON.stringify(body)); // ← ends the response
}
Because it ends the response, Nuxt's downstream handler never renders error.vue. This affects all framework errors — a plain page-navigation 404 is not an EvlogError, so it always falls into the JSON branch.
Why the dev / frameworkOverlay option doesn't help
Setting dev: 'both' / dev: 'nitro' (i.e. frameworkOverlay: true) makes the handler call ctx.defaultHandler first:
if (!suppressOverlay && ctx?.defaultHandler) await ctx.defaultHandler(error, event, { silent: false });
but ctx.defaultHandler is Nitro's built-in default handler, not the framework's (Nuxt's) error-page handler — and the code still falls through to endNodeResponse(JSON) afterward for non-EvlogError. So there is no configuration that lets the framework render its error page while evlog is active.
Reproduction
- Fresh Nuxt 4 app with a custom
app/error.vue.
- Add
evlog/nuxt to modules.
- Add a page that throws on the server:
throw createError({ statusCode: 404, statusMessage: 'nope' }).
- Request it with
Accept: text/html (a normal browser navigation).
Expected: app/error.vue renders (HTML, status 404).
Actual: raw JSON body, Content-Type: application/json.
Expected behavior
For non-EvlogError errors on document/page requests (e.g. Accept: text/html), evlog should log the wide event but delegate the response to the framework's error handler (let error.vue / the framework error page render) rather than terminating with a JSON body. JSON termination is appropriate for API/EvlogError responses, but not for page navigations.
A few possible directions:
- Only short-circuit with JSON when the request actually wants JSON (
Accept: application/json, /api/**, XHR/fetch), otherwise call through to the framework's real error handler (the next handler in the chain, not Nitro's bare default).
- Or run evlog's handler after the framework's handler and no-op when the response is already ended (logging can happen via the existing Nitro
error hook / plugin regardless).
Workaround
For anyone hitting this, a small Nuxt inline module registered right after evlog/nuxt strips evlog's handler from the chain so the framework's error page renders again (evlog's request/error logging is unaffected — it runs via the separate Nitro plugin):
function restoreFrameworkErrorPage(_opts, nuxt) {
nuxt.hook('nitro:config', (nitroConfig) => {
if (Array.isArray(nitroConfig.errorHandler)) {
nitroConfig.errorHandler = nitroConfig.errorHandler.filter(
h => !String(h).includes('/evlog/'),
);
}
});
}
// modules: [ ..., 'evlog/nuxt', restoreFrameworkErrorPage ]
Environment
evlog: 2.19.1nuxt: 4.4.8 (compatibilityVersion 5)nitropack: 2.13.4Summary
With
evlog/nuxtinstalled, every SSR error (404/500) returns a raw JSON body instead of rendering the framework's error page (app/error.vuein Nuxt). A user navigating to a page that throwscreateError({ statusCode: 404 })sees this in the browser:{"url":"/user/does-not-exist","status":404,"statusCode":404,"statusText":"...","statusMessage":"...","message":"...","error":true}instead of the custom error page. Removing
evlog/nuxtimmediately restores correct error-page rendering.Root cause
src/nuxt/module.tsregisters evlog's Nitro error handler by prepending it to whatever is already configured:In a Nuxt app, Nuxt registers its own Nitro error handler (
@nuxt/nitro-server/.../handlers/error) which renderserror.vue. After evlog's hook runs, the resolved chain is:Nitro executes handlers in order. evlog's handler (
src/nitro/errorHandler.ts) runs first and, for any non-EvlogError, unconditionally builds a JSON body and ends the Node response:Because it ends the response, Nuxt's downstream handler never renders
error.vue. This affects all framework errors — a plain page-navigation 404 is not anEvlogError, so it always falls into the JSON branch.Why the
dev/frameworkOverlayoption doesn't helpSetting
dev: 'both'/dev: 'nitro'(i.e.frameworkOverlay: true) makes the handler callctx.defaultHandlerfirst:but
ctx.defaultHandleris Nitro's built-in default handler, not the framework's (Nuxt's) error-page handler — and the code still falls through toendNodeResponse(JSON)afterward for non-EvlogError. So there is no configuration that lets the framework render its error page while evlog is active.Reproduction
app/error.vue.evlog/nuxtto modules.throw createError({ statusCode: 404, statusMessage: 'nope' }).Accept: text/html(a normal browser navigation).Expected:
app/error.vuerenders (HTML, status 404).Actual: raw JSON body,
Content-Type: application/json.Expected behavior
For non-
EvlogErrorerrors on document/page requests (e.g.Accept: text/html), evlog should log the wide event but delegate the response to the framework's error handler (leterror.vue/ the framework error page render) rather than terminating with a JSON body. JSON termination is appropriate for API/EvlogErrorresponses, but not for page navigations.A few possible directions:
Accept: application/json,/api/**, XHR/fetch), otherwise call through to the framework's real error handler (the next handler in the chain, not Nitro's bare default).errorhook / plugin regardless).Workaround
For anyone hitting this, a small Nuxt inline module registered right after
evlog/nuxtstrips evlog's handler from the chain so the framework's error page renders again (evlog's request/error logging is unaffected — it runs via the separate Nitro plugin):