diff --git a/.changeset/fix-sourcemap-crash-invalid-column.md b/.changeset/fix-sourcemap-crash-invalid-column.md new file mode 100644 index 0000000000..0820aa616d --- /dev/null +++ b/.changeset/fix-sourcemap-crash-invalid-column.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Prevent `wrangler dev` crash when source-mapping a truncated error chunk + +When a worker logs many errors in quick succession, the stderr chunks received by `wrangler dev` can be truncated mid-stack-frame, leaving a call site with an invalid column number. The source map library throws in that case, which was crashing the wrangler process entirely. The error is now caught and the original (un-source-mapped) text is returned instead. diff --git a/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts index 8aa08433f2..1ca2579300 100644 --- a/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts +++ b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import url from "node:url"; import { maybeGetFile } from "@cloudflare/workers-shared"; import { getFreshSourceMapSupport } from "miniflare"; +import { logger } from "../../shared/context"; import type { Options } from "@cspotcode/source-map-support"; import type Protocol from "devtools-protocol"; @@ -122,6 +123,27 @@ function callFrameToCallSite(frame: Protocol.Runtime.CallFrame): CallSite { }); } +/** + * Calls `prepareStack` and returns `null` if it throws (e.g. when a truncated + * stderr chunk produces an invalid column number). Returning `null` lets + * `getSourceMappedString` fall back to the original string without executing + * the replacement loop against a partially-computed result. + */ +function tryPrepareStack( + prepareStack: ReturnType, + error: Error, + callSites: NodeJS.CallSite[] +): string | null { + try { + return prepareStack(error, callSites); + } catch (err) { + logger?.debug( + `Source map application failed, falling back to original stack trace: ${err}` + ); + return null; + } +} + const placeholderError = new Error(); export function getSourceMappedString( value: string, @@ -138,16 +160,20 @@ export function getSourceMappedString( const callSiteLines = Array.from(value.matchAll(CALL_SITE_REGEXP)); const callSites = callSiteLines.map(lineMatchToCallSite); const prepareStack = getSourceMappingPrepareStackTrace(retrieveSourceMap); - const sourceMappedStackTrace: string = prepareStack( + const sourceMappedStackTrace = tryPrepareStack( + prepareStack, placeholderError, callSites ); + if (sourceMappedStackTrace === null) { + return value; + } const sourceMappedCallSiteLines = sourceMappedStackTrace.split("\n").slice(1); for (let i = 0; i < callSiteLines.length; i++) { // If a call site doesn't have a file name, it's likely invalid, so don't // apply source mapping (see cloudflare/workers-sdk#4668) - if (callSites[i].getFileName() === undefined) { + if (callSites[i].getFileName() === null) { continue; } diff --git a/packages/deploy-helpers/tests/sourcemap.test.ts b/packages/deploy-helpers/tests/sourcemap.test.ts new file mode 100644 index 0000000000..b4fb831aa8 --- /dev/null +++ b/packages/deploy-helpers/tests/sourcemap.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "vitest"; +import { getSourceMappedString } from "../src/deploy/helpers/sourcemap"; + +describe("getSourceMappedString", () => { + it("returns original value when source mapping throws", ({ expect }) => { + const value = `Error: test\n at Object. (/some/file.js:1:1)`; + + const result = getSourceMappedString(value, () => { + throw new Error("simulated source map failure"); + }); + + expect(result).toBe(value); + }); +});