Skip to content

[clerk-nuxt] Auto-registered Nitro server middleware breaks h3 auto-imports — ReferenceError: H3Error is not defined #8430

@ericcorriel

Description

@ericcorriel

Preliminary Checks

Reproduction

https://github.com/ericcorriel/clerk-nuxt-bug

Publishable key

pk_test_aW5maW5pdGUtZmxlYS04Ni5jbGVyay5hY2NvdW50cy5kZXYk

Description

Repro repo

https://github.com/ericorriel/clerk-nuxt-bug

Standalone minimal Nuxt 4.4.4 + @clerk/nuxt 2.2.9 project. Clone, yarn install, copy .env.example to .env and paste Clerk dev keys, yarn dev, visit http://localhost:3000/ — the bug fires immediately on first request.

The repo also includes the workaround we shipped (commented-out config block + a hand-rolled middleware in server/middleware/clerk.ts). Uncomment the workaround block in nuxt.config.ts, rm -rf .nuxt, restart, and the page renders cleanly.

Summary

When @clerk/nuxt is added to a Nuxt 4 app's modules array with default settings, every SSR-rendered page throws ReferenceError: H3Error is not defined at module-load time. The Nitro dev bundle's auto-imports virtual module references h3 helpers (H3Error, H3Event, appendCorsHeaders, and others) without emitting the corresponding import statements at the top of the bundle.

Disabling Clerk's auto-registered server middleware via clerk: { skipServerMiddleware: true } is a workaround, but importing clerkMiddleware from @clerk/nuxt/server to register the middleware manually re-triggers the bug. Only avoiding @clerk/nuxt/server entirely (and using @clerk/backend directly) sidesteps it.

Reproduced on

Package Versions tested
nuxt 4.3.1, 4.4.4 (latest stable)
@clerk/nuxt 2.2.8, 2.2.9 (latest stable)
@nuxt/nitro-server 4.3.1, 4.4.4
unimport 5.6.0, 5.7.0, 6.2.0 (latest)
h3 1.15.6

The bug is consistent across all combinations above.

Stack trace

ReferenceError: H3Error is not defined
    at /path/to/.nuxt/dev/index.mjs:7673:12
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)

The line number varies slightly with bundle size; the symbol may also be H3Event, appendCorsHeaders, setHeaders, etc. — anything in @nuxt/nitro-server's h3 auto-imports preset that user code doesn't already import explicitly.

Diagnosis

Inspecting the generated .nuxt/dev/index.mjs, the bundle contains an auto-imports virtual module:

const _virtual__imports = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  // ... user-defined symbols ...
  H3Error: H3Error,        // <-- ReferenceError fires here
  H3Event: H3Event,
  appendCorsHeaders: appendCorsHeaders,
  // ... more h3 symbols ...
}));

But the top-of-file h3 import only includes the subset of h3 helpers that user code references explicitly:

import { defineEventHandler, createError, eventHandler, /* ... ~30 more ... */ } from 'h3';
// H3Error, H3Event, appendCorsHeaders NOT in this list

So H3Error/H3Event/etc. are referenced as values in the virtual imports object but were never imported into the bundle's scope.

@nuxt/nitro-server registers these as auto-imports via:

// node_modules/@nuxt/nitro-server/dist/index.mjs (at line 229 in 4.4.4)
presets: [{
  from: 'h3',
  imports: ['H3Event', 'H3Error']
}, ...]

This preset works correctly when @clerk/nuxt is not in the modules array — the bundler emits the corresponding import statements and everything is fine. Adding @clerk/nuxt is the trigger.

What we tried (none of these worked)

  1. Pinning unimport to 5.6.0, 5.7.0, and ^6.2.0 via yarn resolutions. Same error every time.
  2. Explicit user import: adding import { H3Error, H3Event } from 'h3' to a server plugin file. The bundler tree-shakes the plugin away or unimport strips the import as already-auto-registered. Either way the import statement never appears in the final bundle.
  3. Explicit nitro.imports.imports config with [{ name: 'H3Error', from: 'h3' }, ...]. No effect — the bundle still misses the import.
  4. Stripping the h3 value-preset via a nitro:config hook in nuxt.config.ts. This made H3Error materialize correctly, but the next preset entry (appendCorsHeaders) failed identically — the bug is preset-wide, not specific to particular symbols.
  5. Upgrading Nuxt from 4.3.1 to 4.4.4. Bug persists.
  6. Manual middleware registration in server/middleware/clerk.ts with import { clerkMiddleware } from '@clerk/nuxt/server'. This re-triggers the bug — importing anything from @clerk/nuxt/server is enough; the auto-registration isn't the only path that breaks the bundle.

Workaround that worked

  1. Set clerk: { skipServerMiddleware: true } in nuxt.config.ts.
  2. Hand-roll a Nitro middleware using @clerk/backend directly:
// server/middleware/clerk.ts
import { createClerkClient } from '@clerk/backend'

let clerkSingleton: ReturnType<typeof createClerkClient> | null = null

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig(event)
  const secretKey = config.clerk?.secretKey
  if (!secretKey) {
    event.context.auth = () => ({ isAuthenticated: false, userId: null, sessionId: null, sessionClaims: null })
    return
  }
  if (!clerkSingleton) {
    clerkSingleton = createClerkClient({
      secretKey,
      publishableKey: config.public?.clerk?.publishableKey
    })
  }
  try {
    const requestState = await clerkSingleton.authenticateRequest(toWebRequest(event), { acceptsToken: 'any' })
    event.context.auth = () => requestState.toAuth()
    if (requestState.headers) {
      requestState.headers.forEach((value, key) => setResponseHeader(event, key, value))
    }
  } catch {
    event.context.auth = () => ({ isAuthenticated: false, userId: null, sessionId: null, sessionClaims: null })
  }
})

This is functionally identical to the clerkMiddleware() export from @clerk/nuxt/server for our use case (we don't use keyless mode or the handshake redirect flow). Importantly, it imports only from @clerk/backend (a transitive dep we already pull in via @clerk/nuxt) and from h3 — no @clerk/nuxt/server path is involved at the bundle level.

With this in place, the bundler emits import { ... } from 'h3' correctly with all the symbols the auto-imports virtual module references, the bundle loads, and event.context.auth() works exactly as before.

Hypothesis

Something in @clerk/nuxt's server-runtime entry or its internal exports interacts with unimport's scanner in a way that registers h3 auto-imports as "tracked" but somehow short-circuits the bundler's import-injection step. The corruption manifests in any code path that pulls @clerk/nuxt/server into the bundle — auto-registered middleware, manually-registered middleware via clerkMiddleware() import, possibly other entry points.

I haven't isolated the exact mechanism, but the workaround above sidesteps it cleanly by using @clerk/backend directly. Happy to add more diagnostics if helpful.

Environment

  • macOS 26 (Apple Silicon)
  • Node.js (latest LTS)
  • yarn 1.22.22
  • Nuxt 4.4.4 with Vite/Nitro defaults
  • Other Nuxt modules in our production project: shadcn-nuxt, @nuxtjs/tailwindcss, @nuxtjs/color-mode. Removing these does not affect the bug — the standalone repro repo above includes none of them.

Impact

For any Nuxt 4 app adopting @clerk/nuxt server-side, this is a hard blocker on default usage — every SSR page crashes. The workaround restores functionality but adds maintenance burden (the project has to track when the upstream fix lands and remove the workaround). It also subtly changes behavior on edge cases not covered by the hand-rolled middleware (keyless mode, handshake redirects).

A Nuxt-4 + Clerk migration was the trigger that surfaced this for us; we're shipping the workaround in production while we wait for an upstream fix. Happy to test patches or canaries against the repro repo.

Environment

- macOS 26 (Apple Silicon)
- Node.js (latest LTS)
- yarn 1.22.22
- Nuxt 4.4.4 with Vite/Nitro defaults
- Other Nuxt modules in our production project: `shadcn-nuxt`, `@nuxtjs/tailwindcss`, `@nuxtjs/color-mode`. Removing these does not affect the bug — the standalone repro repo above includes none of them.

Metadata

Metadata

Assignees

Labels

needs-triageA ticket that needs to be triaged by a team member

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions