Skip to content

[bug] Nuxt error handler returns JSON instead of rendering error.vue for all SSR errors #390

Description

@nathanchase

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

  1. Fresh Nuxt 4 app with a custom app/error.vue.
  2. Add evlog/nuxt to modules.
  3. Add a page that throws on the server: throw createError({ statusCode: 404, statusMessage: 'nope' }).
  4. 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 ]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions