diff --git a/docs/api/http.md b/docs/api/http.md index eba77a16c1..17339c5646 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -78,7 +78,7 @@ Bun.serve({ fetch(req) { throw new Error("woops!"); }, - error(error: Error, req: Request | null) { + error(error) { return new Response(`
${error}\n${error.stack}
`, { headers: { "Content-Type": "text/html", diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d62bd911c2..c8a303fb43 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2194,7 +2194,7 @@ declare module "bun" { */ development?: boolean; - error?: (this: Server, error: ErrorLike, request: Request | null) => Response | Promise; + error?: (this: Server, request: ErrorLike) => Response | Promise | undefined | Promise; /** * Uniquely identify a server instance with an ID diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index a913834768..b2b8e0465f 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1373,7 +1373,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (!has_responded) ctx.runErrorHandler( value, - JSValue.jsNull(), ); if (ctx.flags.aborted) { @@ -1999,20 +1998,18 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp file.pathlike.fd else switch (bun.sys.open(file.pathlike.path.sliceZ(&file_buf), std.os.O.RDONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC, 0)) { .result => |_fd| _fd, - .err => |err| return this.runErrorHandler( - err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance(this.server.globalThis), - JSValue.jsNull(), - ), + .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( + this.server.globalThis, + )), }; // stat only blocks if the target is a file descriptor const stat: bun.Stat = switch (bun.sys.fstat(fd)) { .result => |result| result, .err => |err| { - this.runErrorHandler( - err.withPathLike(file.pathlike).toSystemError().toErrorInstance(this.server.globalThis), - JSValue.jsNull(), - ); + this.runErrorHandler(err.withPathLike(file.pathlike).toSystemError().toErrorInstance( + this.server.globalThis, + )); if (auto_close) { _ = bun.sys.close(fd); } @@ -2032,10 +2029,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }; var sys = err.withPathLike(file.pathlike).toSystemError(); sys.message = bun.String.static("MacOS does not support sending non-regular files"); - this.runErrorHandler( - sys.toErrorInstance(this.server.globalThis), - JSValue.jsNull(), - ); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); return; } } @@ -2052,10 +2048,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }; var sys = err.withPathLike(file.pathlike).toSystemError(); sys.message = bun.String.static("File must be regular or FIFO"); - this.runErrorHandler( - sys.toErrorInstance(this.server.globalThis), - JSValue.jsNull(), - ); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); return; } } @@ -2132,10 +2127,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } if (result == .err) { - this.runErrorHandler( - result.err.toErrorInstance(this.server.globalThis), - JSValue.jsNull(), - ); + this.runErrorHandler(result.err.toErrorInstance(this.server.globalThis)); return; } @@ -2459,7 +2451,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } if (response_value.toError()) |err_value| { - ctx.runErrorHandler(err_value, request_value); + ctx.runErrorHandler(err_value); return; } @@ -2674,10 +2666,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.finalizeForAbort(); return; } - this.runErrorHandler( - err, - JSC.JSValue.jsNull(), - ); + this.runErrorHandler(err); return; }, // .InlineBlob, @@ -2709,10 +2698,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .message = bun.String.static("Stream already used, please create a new one"), }; stream.value.unprotect(); - this.runErrorHandler( - err.toErrorInstance(this.server.globalThis), - JSC.JSValue.jsNull(), - ); + this.runErrorHandler(err.toErrorInstance(this.server.globalThis)); return; } @@ -2896,9 +2882,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp pub fn runErrorHandler( this: *RequestContext, value: JSC.JSValue, - request_value: JSValue, ) void { - runErrorHandlerWithStatusCode(this, value, 500, request_value); + runErrorHandlerWithStatusCode(this, value, 500); } const PathnameFormatter = struct { @@ -2959,20 +2944,14 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this: *RequestContext, value: JSC.JSValue, status: u16, - request_value: JSValue, ) void { JSC.markBinding(@src()); if (!this.server.config.onError.isEmpty() and !this.flags.has_called_error_handler) { this.flags.has_called_error_handler = true; - - if (Environment.allow_assert) std.debug.assert(request_value != .zero); const result = this.server.config.onError.callWithThis( this.server.globalThis, this.server.thisObject, - &.{ - value, - request_value, - }, + &.{value}, ); defer result.ensureStillAlive(); if (!result.isEmptyOrUndefinedOrNull()) { @@ -3065,12 +3044,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this: *RequestContext, value: JSC.JSValue, status: u16, - request_value: JSValue, ) void { JSC.markBinding(@src()); if (this.resp == null or this.resp.?.hasResponded()) return; - runErrorHandlerWithStatusCodeDontCheckResponded(this, value, status, request_value); + runErrorHandlerWithStatusCodeDontCheckResponded(this, value, status); } pub fn renderMetadata(this: *RequestContext) void { @@ -3366,24 +3344,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return (this.resp orelse return null).getRemoteSocketInfo(); } - pub fn toRequestObject(this: *RequestContext, globalThis: *JSGlobalObject) !*JSC.WebCore.Request { - if (Environment.allow_assert) std.debug.assert(this.signal == null); - this.signal = JSC.WebCore.AbortSignal.new(globalThis); - - if (Environment.allow_assert) std.debug.assert(this.request_body == null); - this.request_body = JSC.WebCore.InitRequestBodyValue(.{ .Null = {} }) catch unreachable; - - const request_object = try this.allocator.create(JSC.WebCore.Request); - request_object.* = .{ - .method = this.method, - .request_context = AnyRequestContext.init(this), - .https = ssl_enabled, - .signal = this.signal.?.ref(), - .body = this.request_body.?.ref(), - }; - return request_object; - } - pub const Export = shim.exportFunctions(.{ .onResolve = onResolve, .onReject = onReject, @@ -5899,7 +5859,20 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp var ctx = this.request_pool_allocator.tryGet() catch @panic("ran out of memory"); ctx.create(this, req, resp); this.vm.jsc.reportExtraMemory(@sizeOf(RequestContext)); - const request_object = ctx.toRequestObject(this.globalThis) catch unreachable; + var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + var body = JSC.WebCore.InitRequestBodyValue(.{ .Null = {} }) catch unreachable; + + ctx.request_body = body; + var signal = JSC.WebCore.AbortSignal.new(this.globalThis); + ctx.signal = signal; + + request_object.* = .{ + .method = ctx.method, + .request_context = AnyRequestContext.init(ctx), + .https = ssl_enabled, + .signal = signal.ref(), + .body = body.ref(), + }; if (comptime debug_mode) { ctx.flags.is_web_browser_navigation = brk: { @@ -5957,8 +5930,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp request_object.toJS(this.globalThis), this.thisObject, }; + const request_value = args[0]; request_value.ensureStillAlive(); + const response_value = this.config.onRequest.callWithThis(this.globalThis, this.thisObject, &args); defer { // uWS request will not live longer than this function @@ -6002,7 +5977,21 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp req.setYield(false); var ctx = this.request_pool_allocator.tryGet() catch @panic("ran out of memory"); ctx.create(this, req, resp); - const request_object = ctx.toRequestObject(this.globalThis) catch unreachable; + var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + var body = JSC.WebCore.InitRequestBodyValue(.{ .Null = {} }) catch unreachable; + + ctx.request_body = body; + var signal = JSC.WebCore.AbortSignal.new(this.globalThis); + ctx.signal = signal; + + request_object.* = .{ + .method = ctx.method, + .request_context = AnyRequestContext.init(ctx), + .upgrader = ctx, + .https = ssl_enabled, + .signal = signal.ref(), + .body = body.ref(), + }; ctx.upgrade_context = upgrade_ctx; // We keep the Request object alive for the duration of the request so that we can remove the pointer to the UWS request object. diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index 452dbb7ac9..dc1aa60ecd 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -317,7 +317,6 @@ describe("Server", () => { server.stop(true); } }); - test("abort signal on server with stream", async () => { { let signalOnServer = false; @@ -434,74 +433,6 @@ test("unref keeps process alive for ongoing connections", async () => { expect([path.join(import.meta.dir, "unref-fixture-2.ts")]).toRun(); }); -describe("Bun.serve error handling", () => { - test("supports error handling", async () => { - const server = Bun.serve({ - port: 0, - fetch(req) { - throw new Error("woops!"); - }, - error(error) { - return new Response(`${error.message}`); - }, - }); - - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe("woops!"); - server.stop(true); - }); - - test("supports reading the Request in error handling", async () => { - const server = Bun.serve({ - port: 0, - fetch(req) { - throw new Error("woops!"); - }, - error(error, req) { - if (req === null) return new Response(`${error.message}`); - return new Response(`${error.message}\n${req.method}`); - }, - }); - - const response = await fetch(`http://${server.hostname}:${server.port}`); - expect(await response.text()).toBe("woops!\nGET"); - server.stop(true); - }); - - test("the request headers survive", async () => { - const server = Bun.serve({ - port: 0, - fetch(req) { - throw new Error("woops!"); - }, - error(error, req) { - return new Response(`${req.headers.get("x-foo")}`); - }, - }); - - const response = await fetch(`http://${server.hostname}:${server.port}`, { headers: { "x-foo": "1" } }); - expect(await response.text()).toBe("1"); - server.stop(true); - }); - - test("the request url survives", async () => { - const server = Bun.serve({ - port: 0, - fetch(req) { - throw new Error("woops!"); - }, - error(error, req) { - return new Response(`${req.url}`); - }, - }); - - const url = `http://${server.hostname}:${server.port}/`; - const response = await fetch(url); - expect(await response.text()).toBe(url); - server.stop(true); - }); -}); - test("Bun does not crash when given invalid config", async () => { const server1 = Bun.serve({ fetch(request, server) {