Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
831e80e014 fix: handle missing asset hash gracefully instead of panicking
The bundler was panicking with "cached asset not found" when there was
an inconsistency between IncrementalGraph.bundled_files and Assets.path_map.
This could happen due to race conditions or state inconsistencies.

The fix gracefully handles the case where an asset is cached in bundled_files
but missing from assets.path_map by falling back to normal asset processing
instead of panicking.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-17 14:48:59 +00:00
2 changed files with 98 additions and 8 deletions

View File

@@ -3034,14 +3034,23 @@ pub const BundleV2 = struct {
if (loader == .html and entry.kind == .asset) {
// Overload `path.text` to point to the final URL
// This information cannot be queried while printing because a lock wouldn't get held.
const hash = dev_server.assets.getHash(path.text) orelse @panic("cached asset not found");
import_record.path.text = path.text;
import_record.path.namespace = "file";
import_record.path.pretty = std.fmt.allocPrint(this.graph.allocator, bun.bake.DevServer.asset_prefix ++ "/{s}{s}", .{
&std.fmt.bytesToHex(std.mem.asBytes(&hash), .lower),
std.fs.path.extension(path.text),
}) catch bun.outOfMemory();
import_record.path.is_disabled = false;
if (dev_server.assets.getHash(path.text)) |hash| {
import_record.path.text = path.text;
import_record.path.namespace = "file";
import_record.path.pretty = std.fmt.allocPrint(this.graph.allocator, bun.bake.DevServer.asset_prefix ++ "/{s}{s}", .{
&std.fmt.bytesToHex(std.mem.asBytes(&hash), .lower),
std.fs.path.extension(path.text),
}) catch bun.outOfMemory();
import_record.path.is_disabled = false;
} else {
// Asset is cached in bundled_files but not in assets.path_map
// This can happen due to race conditions or inconsistent state
// Fall back to normal asset processing
import_record.path.text = path.text;
import_record.path.pretty = rel;
import_record.path = this.pathWithPrettyInitialized(path.*, target) catch bun.outOfMemory();
import_record.path.is_disabled = false;
}
} else {
import_record.path.text = path.text;
import_record.path.pretty = rel;

View File

@@ -0,0 +1,81 @@
import { test, expect } from "bun:test";
import { tempDirWithFiles, bunEnv } from "harness";
import path from "path";
test("asset cache race condition should not panic", async () => {
// This test ensures that when there's an inconsistency between bundled_files and assets.path_map,
// the bundler gracefully handles the missing asset hash instead of panicking
const dir = tempDirWithFiles("asset-cache-race", {
"index.html": `
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<img src="./test.png" alt="test">
<div style="background: url('./test.png')"></div>
</body>
</html>
`,
"test.png": Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"base64"
),
"app.ts": `
import './test.png';
console.log("Hello from app.ts");
`,
});
// Test HTML bundling with assets
const proc1 = Bun.spawn({
cmd: [
process.execPath,
"build",
"index.html",
"--outdir=dist",
],
env: bunEnv,
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([
proc1.stdout.text(),
proc1.stderr.text(),
proc1.exited,
]);
// Should not panic or crash
expect(exitCode1).toBe(0);
expect(stderr1).not.toContain("panic");
expect(stderr1).not.toContain("cached asset not found");
// Test TS bundling with assets
const proc2 = Bun.spawn({
cmd: [
process.execPath,
"build",
"app.ts",
"--outdir=dist2",
],
env: bunEnv,
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([
proc2.stdout.text(),
proc2.stderr.text(),
proc2.exited,
]);
// Should not panic or crash
expect(exitCode2).toBe(0);
expect(stderr2).not.toContain("panic");
expect(stderr2).not.toContain("cached asset not found");
});