Compare commits

...

6 Commits

Author SHA1 Message Date
autofix-ci[bot]
35ccdd7f02 [autofix.ci] apply automated fixes 2025-08-25 06:30:05 +00:00
Claude Bot
83a3a905d2 Switch to Accept header-based content negotiation (much cleaner!)
- Replace User-Agent detection with proper Accept header parsing
- Check for text/html and */* to determine response format
- Much more standards-compliant approach
- Cleaner logic and better separation of concerns

Thanks to feedback suggesting Accept header over User-Agent parsing!

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 06:27:39 +00:00
autofix-ci[bot]
e239dcdc54 [autofix.ci] apply automated fixes 2025-08-25 05:55:56 +00:00
Claude Bot
537459129b Update User-Agent detection logic and improve request handling
- Fixed checkRouteFailures to pass request parameter
- Improved User-Agent detection logic
- Test framework improvements (test currently failing but infrastructure is working)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 05:52:59 +00:00
Claude Bot
4741c3eb8f Add regression test for User-Agent detection in dev server errors
The test currently fails but demonstrates the issue. It should pass once
the Zig code changes are compiled and working correctly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 05:09:45 +00:00
Claude Bot
c5872f1535 Fix Bun.serve to return plain text errors for non-browser clients
Fixes #22055 by detecting User-Agent headers and returning appropriate
response formats:

- Browser clients (Mozilla, Chrome, Safari, Firefox, Edge): HTML error page
- Non-browser clients (fetch, curl, etc.): Plain text error summary
- Deferred requests default to HTML for compatibility

This improves the experience when using `fetch()` or similar programmatic
clients that would otherwise receive HTML error pages containing embedded
JavaScript and base64-encoded error data.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 05:00:49 +00:00
5 changed files with 131 additions and 37 deletions

0
1 Normal file
View File

0
DevServer Normal file
View File

7
app.js Normal file
View File

@@ -0,0 +1,7 @@
export default {
port: 3000,
fetch() {
import('./nonexistent.js');
return new Response('hello');
}
}

View File

@@ -1048,7 +1048,7 @@ fn ensureRouteIsBundled(
if (dev.bundling_failures.count() > 0) {
// Trace the graph to see if there are any failures that are
// reachable by this route.
switch (try checkRouteFailures(dev, route_bundle_index, resp)) {
switch (try checkRouteFailures(dev, route_bundle_index, req, resp)) {
.stop => return,
.ok => {}, // Errors were cleared or not in the way.
.rebuild => continue :sw .unqueued, // Do the build all over again
@@ -1060,6 +1060,7 @@ fn ensureRouteIsBundled(
},
.evaluation_failure => {
try dev.sendSerializedFailures(
req,
resp,
(&(dev.routeBundlePtr(route_bundle_index).data.framework.evaluate_failure.?))[0..1],
.evaluation,
@@ -1102,6 +1103,7 @@ fn deferRequest(
fn checkRouteFailures(
dev: *DevServer,
route_bundle_index: RouteBundle.Index,
req: *Request,
resp: anytype,
) !enum { stop, ok, rebuild } {
var sfa_state = std.heap.stackFallback(65536, dev.allocator());
@@ -1129,6 +1131,7 @@ fn checkRouteFailures(
}
try dev.sendSerializedFailures(
req,
resp,
dev.incremental_result.failures_added.items,
.bundler,
@@ -2550,6 +2553,7 @@ pub fn finalizeBundle(
};
try dev.sendSerializedFailures(
null,
resp,
dev.bundling_failures.keys(),
.bundler,
@@ -2928,55 +2932,93 @@ fn encodeSerializedFailures(
}
}
/// Check if the client accepts HTML responses based on Accept header
fn acceptsHtml(req: *Request) bool {
const accept = req.header("accept") orelse return true; // Default to HTML if no Accept header
// Check if client accepts text/html or */*
return std.mem.indexOf(u8, accept, "text/html") != null or
std.mem.indexOf(u8, accept, "*/*") != null;
}
fn sendSerializedFailures(
dev: *DevServer,
req: ?*Request,
resp: AnyResponse,
failures: []const SerializedFailure,
kind: ErrorPageKind,
inspector_agent: ?*BunFrontendDevServerAgent,
) !void {
var buf: std.ArrayList(u8) = try .initCapacity(dev.allocator(), 2048);
errdefer buf.deinit();
// Check if client accepts HTML based on Accept header
// If no request is available, default to HTML (likely from deferred requests)
const wants_html = req == null or acceptsHtml(req.?);
if (wants_html) {
// Return HTML error page for clients that accept HTML
var buf: std.ArrayList(u8) = try .initCapacity(dev.allocator(), 2048);
errdefer buf.deinit();
try buf.appendSlice(switch (kind) {
inline else => |k| std.fmt.comptimePrint(
\\<!doctype html>
\\<html lang="en">
\\<head>
\\<meta charset="UTF-8" />
\\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\\<title>Bun - {[page_title]s}</title>
\\<style>:root{{color-scheme:light dark}}body{{background:light-dark(white,black)}}</style>
\\</head>
\\<body>
\\<noscript><h1 style="font:28px sans-serif;">{[page_title]s}</h1><p style="font:20px sans-serif;">Bun requires JavaScript enabled in the browser to render this error screen, as well as receive hot reloading events.</p></noscript>
\\<script>let error=Uint8Array.from(atob("
,
.{ .page_title = switch (k) {
.bundler => "Build Failed",
.evaluation, .runtime => "Runtime Error",
} },
),
});
try buf.appendSlice(switch (kind) {
inline else => |k| std.fmt.comptimePrint(
\\<!doctype html>
\\<html lang="en">
\\<head>
\\<meta charset="UTF-8" />
\\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\\<title>Bun - {[page_title]s}</title>
\\<style>:root{{color-scheme:light dark}}body{{background:light-dark(white,black)}}</style>
\\</head>
\\<body>
\\<noscript><h1 style="font:28px sans-serif;">{[page_title]s}</h1><p style="font:20px sans-serif;">Bun requires JavaScript enabled in the browser to render this error screen, as well as receive hot reloading events.</p></noscript>
\\<script>let error=Uint8Array.from(atob("
,
.{ .page_title = switch (k) {
.bundler => "Build Failed",
.evaluation, .runtime => "Runtime Error",
} },
),
});
try dev.encodeSerializedFailures(failures, &buf, inspector_agent);
try dev.encodeSerializedFailures(failures, &buf, inspector_agent);
const pre = "\"),c=>c.charCodeAt(0));let config={bun:\"" ++ bun.Global.package_json_version_with_canary ++ "\"};";
const post = "</script></body></html>";
const pre = "\"),c=>c.charCodeAt(0));let config={bun:\"" ++ bun.Global.package_json_version_with_canary ++ "\"};";
const post = "</script></body></html>";
if (Environment.codegen_embed) {
try buf.appendSlice(pre ++ @embedFile("bake-codegen/bake.error.js") ++ post);
if (Environment.codegen_embed) {
try buf.appendSlice(pre ++ @embedFile("bake-codegen/bake.error.js") ++ post);
} else {
try buf.appendSlice(pre);
try buf.appendSlice(bun.runtimeEmbedFile(.codegen_eager, "bake.error.js"));
try buf.appendSlice(post);
}
StaticRoute.sendBlobThenDeinit(resp, &.fromArrayList(buf), .{
.mime_type = &.html,
.server = dev.server.?,
.status_code = 500,
});
} else {
try buf.appendSlice(pre);
try buf.appendSlice(bun.runtimeEmbedFile(.codegen_eager, "bake.error.js"));
try buf.appendSlice(post);
}
// Return plain text error for clients that don't accept HTML
var buf: std.ArrayList(u8) = try .initCapacity(dev.allocator(), 512);
errdefer buf.deinit();
StaticRoute.sendBlobThenDeinit(resp, &.fromArrayList(buf), .{
.mime_type = &.html,
.server = dev.server.?,
.status_code = 500,
});
const page_title = switch (kind) {
.bundler => "Build Failed",
.evaluation, .runtime => "Runtime Error",
};
try buf.writer().print("{s}\n\n", .{page_title});
try buf.writer().print("Bun development server encountered an error.\n", .{});
try buf.writer().print("Found {d} error(s) preventing the application from running.\n\n", .{failures.len});
try buf.writer().print("To see detailed error information, open this URL in a web browser.\n", .{});
try buf.writer().print("For programmatic access to error details, consider using WebSocket connections.\n", .{});
StaticRoute.sendBlobThenDeinit(resp, &.fromArrayList(buf), .{
.mime_type = &.text,
.server = dev.server.?,
.status_code = 500,
});
}
}
fn sendBuiltInNotFound(resp: anytype) void {

View File

@@ -0,0 +1,45 @@
import { expect } from "bun:test";
import { devTest, minimalFramework } from "../bake-harness";
// Test for issue #22055: https://github.com/oven-sh/bun/issues/22055
// Development server should return plain text errors for clients that don't accept HTML
devTest("returns appropriate errors based on Accept header (#22055)", {
framework: minimalFramework,
files: {
"routes/index.ts": `
export default function (req, meta) {
// Create a syntax error to trigger the error page
import('./nonexistent-module.js');
return new Response('Hello World');
}
`,
},
async test(dev) {
// Test with Accept: text/html (should get HTML)
const htmlResponse = await dev.fetch("/", {
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
},
});
expect(htmlResponse.status).toBe(500);
const htmlText = await htmlResponse.text();
expect(htmlResponse.headers.get("content-type")).toContain("text/html");
expect(htmlText).toContain("<!doctype html>");
// Test without HTML in Accept header (should get plain text)
const plainResponse = await dev.fetch("/", {
headers: {
"Accept": "application/json, text/plain",
},
});
expect(plainResponse.status).toBe(500);
const plainText = await plainResponse.text();
expect(plainResponse.headers.get("content-type")).toContain("text/plain");
expect(plainText).toContain("Build Failed");
expect(plainText).toContain("Bun development server encountered an error");
expect(plainText).not.toContain("<!doctype html>");
expect(plainText).not.toContain("<script>");
},
});