Skip to content

@sentry/nuxt: maps removed by filesToDeleteAfterUpload stay in Nitro's public-asset manifest, so /_nuxt/*.js.map returns 500 instead of 404 #25242

Description

@mketiku

Is there an existing issue for this?

How do you use Sentry?

Sentry SaaS (sentry.io)

Which SDK are you using?

@sentry/nuxt

SDK Version

11.6.0 (also reproduced on 10.75.0)

Framework Version

Nuxt 3.21.11, nitropack 2.13.4, node-server preset, Node 22

Link to Sentry event

n/a

Reproduction Example/SDK Setup

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@sentry/nuxt/module'],
  sourcemap: { client: 'hidden' },
  sentry: {
    org: '...',
    project: '...',
    sourcemaps: {
      filesToDeleteAfterUpload: ['.output/public/**/*.map'],
    },
  },
});

No auth token is needed to reproduce, because the deletion runs either way.

Steps to Reproduce

  1. nuxt build
  2. find .output/public -name '*.map' finds nothing, because the maps were deleted.
  3. grep -o '"/_nuxt/[^"]*\.js\.map"' .output/server/chunks/nitro/nitro.mjs | head still lists every map in Nitro's public-asset manifest.
  4. node .output/server/index.mjs
  5. Request a real chunk with curl -i localhost:3000/_nuxt/<chunk>.js, then the same URL with .map appended: curl -i localhost:3000/_nuxt/<chunk>.js.map.

Expected Result

The .map request returns 404, the same as an unknown path (/_nuxt/doesnotexist.js.map already returns 404).

Actual Result

The .map request returns 500, with the server log showing:

[request error] [unhandled] [GET] http://localhost:3000/_nuxt/<chunk>.js.map
 H3Error: ENOENT: no such file or directory, open '.../.output/public/_nuxt/<chunk>.js.map'

The 500 response also carries the deleted file's ETag, its Content-Length, and Cache-Control: public, max-age=31536000, immutable, all taken from the stale manifest entry.

Additional Context

Ordering problem:

  1. @nuxt/nitro-server copies the client build into .output/public in Nitro's rollup:before hook (dist/index.mjs:780-783: copyPublicAssets, then nitro:build:public-assets).
  2. Nitro then globs .output/public into the virtual #nitro-internal-virtual/public-assets-data manifest (nitropack/dist/rollup/index.mjs ~L1232) and bundles it into the server.
  3. @sentry/nuxt deletes the files in Nuxt's close hook, after the server bundle exists (build/module/module.mjs L620-623 in 11.6.0, deleteSourceMapsAfterBuild).
  4. At runtime the static handler finds the manifest entry, sets headers, and calls readAsset, which runs fsp.readFile and throws ENOENT. That surfaces as a 500 (nitropack/dist/runtime/internal/static.mjs).

The automatic deletion fallback (when filesToDeleteAfterUpload is unset) uses the same close hook, so it should behave the same way.

hidden maps (#13993) stop browsers from requesting maps, but crawlers and scanners still request them. They get 500s, and Sentry's own server SDK reports those 500s as errors. Another project diagnosed the same chain independently after a bot requested about 47 maps and triggered an alert burst (kodes-agency/reputation-key#631).

Workaround we use, which removes the maps after the copy and before the manifest is built:

hooks: {
  async 'nitro:build:public-assets'(nitro) {
    const { readdir, rm } = await import('node:fs/promises');
    const { join } = await import('node:path');
    const dir = nitro.options.output.publicDir;
    const files = await readdir(dir, { recursive: true });
    await Promise.all(files.filter(f => f.endsWith('.map')).map(f => rm(join(dir, f))));
  },
},

The client upload is unaffected because the Vite plugin uploads from .nuxt/dist/client at writeBundle, before Nitro runs. We confirmed in production that the upload succeeds and the URLs return 404. nitro.ignore: ['**/*.map'] does not work, because Nuxt appends !<buildDir>/dist/client/_nuxt/**/* to nitro.ignore (@nuxt/nitro-server/dist/index.mjs:400).

Suggested fix: for client maps, have the module delete in nitro:build:public-assets (or exclude *.map when Nitro builds the manifest) instead of in close. Server maps can keep the current timing.

Priority

Low. Nothing leaks, but it produces stray 500s and alert noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions