Skip to content

extractSourcemapFromFile crashes the run on a malformed source map, exiting non-zero with every test passing #10892

Description

@nickels

Describe the bug

extractSourcemapFromFile calls convertSourceMap.fromSource() without a try/catch:

// packages/utils/src/source-map.ts  (dist/source-map/node.js)
function extractSourcemapFromFile(code, filePath) {
  const map = (convertSourceMap.fromSource(code)
    || convertSourceMap.fromMapFileSource(code, createConvertSourceMapReadMap(filePath)))?.toObject();
  return map ? { map } : undefined;
}

fromSource() throws when a file contains something that looks like an inline source map but is not one. Its regex matches sourceMappingURL=data:application/json;base64, anywhere at end-of-line, including inside a string literal in minified code. It then base64-decodes the remainder of the file and JSON.parses the result:

SyntaxError: Unexpected token '<binary>', "<binary>" is not valid JSON
    at new Converter (convert-source-map/index.js:83:15)
    at Object.exports.fromComment (convert-source-map/index.js:181:10)
    at Object.exports.fromSource (convert-source-map/index.js:207:22)
    at extractSourcemapFromFile (@vitest/utils/dist/source-map/node.js:8:32)
    at Object.getSourceMap (vitest/dist/chunks/cli-api...js)
    at parseStacktrace (@vitest/utils/dist/source-map.js:387:16)
    at parseErrorStacktrace (@vitest/utils/dist/source-map.js:438:51)

This is reached from parseErrorStacktrace, so it fires while Vitest is formatting an error. Nothing awaits it, so it surfaces as an unhandled rejection and exits the run with code 1 — and it discards the error it was trying to print.

Impact

The failure mode is badly misleading:

  • The run exits non-zero, so CI is red.
  • Every test passed. Our JUnit output read tests 1388, failures 0, errors 0, skipped 0, and the CI server's own test API agreed: total 1388, passed 1388, failed 0.
  • The console shows Errors 2 and two Test Run Error blocks whose stacks contain only Vitest internals. No test is ever named, because the error that would have named one is the thing that got swallowed.
  • It only reproduces where the offending file ends up in a stack frame, so it can be environment-specific. Ours was green on macOS and red on Linux CI for days.

The arithmetic is the only real tell: 1384 passed (1388) next to failures 0. Those reconcile only if four tests never ran, i.e. the runner died rather than a test failing.

Reproduction

tsx triggers it. Its minified register bundles contain that literal string (unsurprisingly — it is a loader that writes source-map comments), so any project using tsx as a setupFile or loader can hit it as soon as an error is formatted with a tsx frame on the stack.

Minimal, two dependencies, no test runner needed:

npm init -y
npm i convert-source-map@2.0.0 tsx@4.23.11
const fs = require('fs'), path = require('path');
const conv = require('convert-source-map');
const dist = 'node_modules/tsx/dist';
for (const f of fs.readdirSync(dist)) {
  if (!/\.(cjs|mjs|js)$/.test(f)) continue;
  const code = fs.readFileSync(path.join(dist, f), 'utf8');
  if (!/sourceMappingURL=data:/.test(code)) continue;
  try { conv.fromSource(code); }
  catch (e) { console.log('THROWS', f, '->', e.message); }
}

Output:

THROWS register-CeSc3Ioe.cjs -> Unexpected token '<binary>', "<binary>" is not valid JSON
THROWS register-DyKhpi9B.mjs -> Unexpected token '<binary>', "<binary>" is not valid JSON

Not specific to one release: reproduced on tsx 4.23.1, 4.23.6 and 4.23.11, under different content-hashed filenames each time.

Suggested fix

A source map is an optional nicety. Failing to read one should degrade to "no source map", never take down the run:

function extractSourcemapFromFile(code, filePath) {
  try {
    const map = (convertSourceMap.fromSource(code)
      || convertSourceMap.fromMapFileSource(code, createConvertSourceMapReadMap(filePath)))?.toObject();
    return map ? { map } : undefined;
  } catch {
    return undefined;
  }
}

That returns exactly what a file with no source map returns. createConvertSourceMapReadMap already has a comment about falling back to an empty source map "to avoid errors" for an invalid filename, so the surrounding code clearly intends this call to be non-fatal; it just does not cover a malformed payload.

Worth considering as well: onStackTrace looks like it should let a user filter these frames out, but it does not help here, because the source map is read before the filter is applied.

I have been running the try/catch above as a package patch for a day across a 1388-test suite with no ill effects, and am happy to open a PR if useful.

Version

vitest 4.1.10, @vitest/utils 4.1.10, convert-source-map 2.0.0, Node 24.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions