const std = @import("std"); const Api = @import("../../api/schema.zig").Api; const bun = @import("bun"); const RequestContext = @import("../../http.zig").RequestContext; const MimeType = @import("../../http.zig").MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = @import("bun").HTTP; const NetworkThread = HTTPClient.NetworkThread; const AsyncIO = NetworkThread.AsyncIO; const JSC = @import("bun").JSC; const js = JSC.C; const Method = @import("../../http/method.zig").Method; const FetchHeaders = JSC.FetchHeaders; const ObjectPool = @import("../../pool.zig").ObjectPool; const SystemError = JSC.SystemError; const Output = @import("bun").Output; const MutableString = @import("bun").MutableString; const strings = @import("bun").strings; const string = @import("bun").string; const default_allocator = @import("bun").default_allocator; const FeatureFlags = @import("bun").FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; const NewClass = @import("../base.zig").NewClass; const d = @import("../base.zig").d; const castObj = @import("../base.zig").castObj; const getAllocator = @import("../base.zig").getAllocator; const JSPrivateDataPtr = @import("../base.zig").JSPrivateDataPtr; const GetJSPrivateData = @import("../base.zig").GetJSPrivateData; const Environment = @import("../../env.zig"); const ZigString = JSC.ZigString; const IdentityContext = @import("../../identity_context.zig").IdentityContext; const JSPromise = JSC.JSPromise; const JSValue = JSC.JSValue; const JSError = JSC.JSError; const JSGlobalObject = JSC.JSGlobalObject; const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator; const VirtualMachine = JSC.VirtualMachine; const Task = JSC.Task; const JSPrinter = bun.js_printer; const picohttp = @import("bun").picohttp; const StringJoiner = @import("../../string_joiner.zig"); const uws = @import("bun").uws; const InlineBlob = JSC.WebCore.InlineBlob; const AnyBlob = JSC.WebCore.AnyBlob; const InternalBlob = JSC.WebCore.InternalBlob; const BodyMixin = JSC.WebCore.BodyMixin; const Body = JSC.WebCore.Body; const Request = JSC.WebCore.Request; const Blob = JSC.WebCore.Blob; pub const Response = struct { const ResponseMixin = BodyMixin(@This()); pub usingnamespace JSC.Codegen.JSResponse; allocator: std.mem.Allocator, body: Body, url: string = "", status_text: string = "", redirected: bool = false, // We must report a consistent value for this reported_estimated_size: ?u63 = null, pub const getText = ResponseMixin.getText; pub const getBody = ResponseMixin.getBody; pub const getBodyUsed = ResponseMixin.getBodyUsed; pub const getJSON = ResponseMixin.getJSON; pub const getArrayBuffer = ResponseMixin.getArrayBuffer; pub const getBlob = ResponseMixin.getBlob; pub const getFormData = ResponseMixin.getFormData; pub fn getFormDataEncoding(this: *Response) ?*bun.FormData.AsyncFormData { var content_type_slice: ZigString.Slice = this.getContentType() orelse return null; defer content_type_slice.deinit(); const encoding = bun.FormData.Encoding.get(content_type_slice.slice()) orelse return null; return bun.FormData.AsyncFormData.init(this.allocator, encoding) catch unreachable; } pub fn estimatedSize(this: *Response) callconv(.C) usize { return this.reported_estimated_size orelse brk: { this.reported_estimated_size = @intCast( u63, this.body.value.estimatedSize() + this.url.len + this.status_text.len + @sizeOf(Response), ); break :brk this.reported_estimated_size.?; }; } pub fn getBodyValue( this: *Response, ) *Body.Value { return &this.body.value; } pub fn getFetchHeaders( this: *Response, ) ?*FetchHeaders { return this.body.init.headers; } pub inline fn statusCode(this: *const Response) u16 { return this.body.init.status_code; } pub fn redirectLocation(this: *const Response) ?[]const u8 { return this.header(.Location); } pub fn header(this: *const Response, name: JSC.FetchHeaders.HTTPHeaderName) ?[]const u8 { return if ((this.body.init.headers orelse return null).fastGet(name)) |str| str.slice() else null; } pub const Props = struct {}; pub fn writeFormat(this: *const Response, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { const Writer = @TypeOf(writer); try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())}); { formatter.indent += 1; defer formatter.indent -|= 1; try formatter.writeIndent(Writer, writer); try writer.writeAll("ok: "); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("url: \""); try writer.print(comptime Output.prettyFmt("{s}", enable_ansi_colors), .{this.url}); try writer.writeAll("\""); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("statusText: "); try JSPrinter.writeJSONString(this.status_text, Writer, writer, .ascii); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("redirected: "); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); formatter.resetLine(); try this.body.writeFormat(Formatter, formatter, writer, enable_ansi_colors); } try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll("}"); formatter.resetLine(); } pub fn isOK(this: *const Response) bool { return this.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299); } pub fn getURL( this: *Response, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/url return ZigString.init(this.url).toValueGC(globalThis); } pub fn getResponseType( this: *Response, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { if (this.body.init.status_code < 200) { return ZigString.init("error").toValue(globalThis); } return ZigString.init("default").toValue(globalThis); } pub fn getStatusText( this: *Response, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText return ZigString.init(this.status_text).withEncoding().toValueGC(globalThis); } pub fn getRedirected( this: *Response, _: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/redirected return JSValue.jsBoolean(this.redirected); } pub fn getOK( this: *Response, _: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok return JSValue.jsBoolean(this.isOK()); } fn getOrCreateHeaders(this: *Response, globalThis: *JSC.JSGlobalObject) *FetchHeaders { if (this.body.init.headers == null) { this.body.init.headers = FetchHeaders.createEmpty(); if (this.body.value == .Blob) { const content_type = this.body.value.Blob.content_type; if (content_type.len > 0) { this.body.init.headers.?.put("content-type", content_type, globalThis); } } } return this.body.init.headers.?; } pub fn getHeaders( this: *Response, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { return this.getOrCreateHeaders(globalThis).toJS(globalThis); } pub fn doClone( this: *Response, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSValue { var cloned = this.clone(getAllocator(globalThis), globalThis); const val = Response.makeMaybePooled(globalThis, cloned); if (this.body.init.headers) |headers| { cloned.body.init.headers = headers.cloneThis(globalThis); } return val; } pub fn makeMaybePooled(globalObject: *JSC.JSGlobalObject, ptr: *Response) JSValue { return ptr.toJS(globalObject); } pub fn cloneInto( this: *Response, new_response: *Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, ) void { new_response.* = Response{ .allocator = allocator, .body = this.body.clone(globalThis), .url = allocator.dupe(u8, this.url) catch unreachable, .status_text = allocator.dupe(u8, this.status_text) catch unreachable, .redirected = this.redirected, }; } pub fn clone(this: *Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Response { var new_response = allocator.create(Response) catch unreachable; this.cloneInto(new_response, allocator, globalThis); return new_response; } pub fn getStatus( this: *Response, _: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/status return JSValue.jsNumber(this.body.init.status_code); } pub fn finalize( this: *Response, ) callconv(.C) void { this.body.deinit(this.allocator); var allocator = this.allocator; if (this.status_text.len > 0) { allocator.free(this.status_text); } if (this.url.len > 0) { allocator.free(this.url); } allocator.destroy(this); } pub fn mimeType(response: *const Response, request_ctx_: ?*const RequestContext) string { return mimeTypeWithDefault(response, MimeType.other, request_ctx_); } pub fn mimeTypeWithDefault(response: *const Response, default: MimeType, request_ctx_: ?*const RequestContext) string { if (response.header(.ContentType)) |content_type| { return content_type; } if (request_ctx_) |request_ctx| { if (request_ctx.url.extname.len > 0) { return MimeType.byExtension(request_ctx.url.extname).value; } } switch (response.body.value) { .Blob => |blob| { if (blob.content_type.len > 0) { return blob.content_type; } // auto-detect HTML if unspecified if (strings.hasPrefixComptime(response.body.value.slice(), "")) { return MimeType.html.value; } return default.value; }, // .InlineBlob => { // // auto-detect HTML if unspecified // if (strings.hasPrefixComptime(response.body.value.slice(), "")) { // return MimeType.html.value; // } // return response.body.value.InlineBlob.contentType(); // }, .InternalBlob => { // auto-detect HTML if unspecified if (strings.hasPrefixComptime(response.body.value.slice(), "")) { return MimeType.html.value; } return response.body.value.InternalBlob.contentType(); }, .Null, .Used, .Locked, .Empty, .Error => return default.value, } } pub fn getContentType( this: *Response, ) ?ZigString.Slice { if (this.body.init.headers) |headers| { if (headers.fastGet(.ContentType)) |value| { return value.toSlice(bun.default_allocator); } } if (this.body.value == .Blob) { if (this.body.value.Blob.content_type.len > 0) return ZigString.Slice.fromUTF8NeverFree(this.body.value.Blob.content_type); } return null; } pub fn constructJSON( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSValue { const args_list = callframe.arguments(2); // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]); // var response = getAllocator(globalThis).create(Response) catch unreachable; var response = Response{ .body = Body{ .init = Body.Init{ .status_code = 200, }, .value = .{ .Empty = {} }, }, .allocator = getAllocator(globalThis), .url = "", }; const json_value = args.nextEat() orelse JSC.JSValue.zero; if (@enumToInt(json_value) != 0) { var zig_str = JSC.ZigString.init(""); // calling JSON.stringify on an empty string adds extra quotes // so this is correct json_value.jsonStringify(globalThis.ptr(), 0, &zig_str); if (zig_str.len > 0) { const allocator = getAllocator(globalThis); var zig_str_slice = zig_str.toSlice(allocator); if (zig_str_slice.isAllocated()) { response.body.value = .{ .Blob = Blob.initWithAllASCII(zig_str_slice.mut(), allocator, globalThis.ptr(), false), }; } else { response.body.value = .{ .Blob = Blob.initWithAllASCII(allocator.dupe(u8, zig_str_slice.slice()) catch unreachable, allocator, globalThis.ptr(), true), }; } } } if (args.nextEat()) |init| { if (init.isUndefinedOrNull()) {} else if (init.isNumber()) { response.body.init.status_code = @intCast(u16, @min(@max(0, init.toInt32()), std.math.maxInt(u16))); } else { if (Body.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| { response.body.init = _init; } } } var headers_ref = response.getOrCreateHeaders(globalThis); headers_ref.putDefault("content-type", MimeType.json.value, globalThis); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; return ptr.toJS(globalThis); } pub fn constructRedirect( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSValue { var args_list = callframe.arguments(4); // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]); // var response = getAllocator(globalThis).create(Response) catch unreachable; var response = Response{ .body = Body{ .init = Body.Init{ .status_code = 302, }, .value = .{ .Empty = {} }, }, .allocator = getAllocator(globalThis), .url = "", }; const url_string_value = args.nextEat() orelse JSC.JSValue.zero; var url_string = ZigString.init(""); if (@enumToInt(url_string_value) != 0) { url_string = url_string_value.getZigString(globalThis.ptr()); } var url_string_slice = url_string.toSlice(getAllocator(globalThis)); defer url_string_slice.deinit(); if (args.nextEat()) |init| { if (init.isUndefinedOrNull()) {} else if (init.isNumber()) { response.body.init.status_code = @intCast(u16, @min(@max(0, init.toInt32()), std.math.maxInt(u16))); } else { if (Body.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| { response.body.init = _init; response.body.init.status_code = 302; } } } response.body.init.headers = response.getOrCreateHeaders(globalThis); var headers_ref = response.body.init.headers.?; headers_ref.put("location", url_string_slice.slice(), globalThis); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; return ptr.toJS(globalThis); } pub fn constructError( globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSValue { var response = getAllocator(globalThis).create(Response) catch unreachable; response.* = Response{ .body = Body{ .init = Body.Init{ .status_code = 0, }, .value = .{ .Empty = {} }, }, .allocator = getAllocator(globalThis), .url = "", }; return response.toJS(globalThis); } pub fn constructor( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) ?*Response { const args_list = brk: { var args = callframe.arguments(2); if (args.len > 1 and args.ptr[1].isEmptyOrUndefinedOrNull()) { args.len = 1; } break :brk args; }; const arguments = args_list.ptr[0..args_list.len]; const body: Body = @as(?Body, brk: { switch (arguments.len) { 0 => { break :brk Body.@"200"(globalThis); }, 1 => { break :brk Body.extract(globalThis, arguments[0]); }, else => { if (arguments[1].isObject()) { break :brk Body.extractWithInit(globalThis, arguments[0], arguments[1]); } std.debug.assert(!arguments[1].isEmptyOrUndefinedOrNull()); const err = globalThis.createTypeErrorInstance("Expected options to be one of: null, undefined, or object", .{}); globalThis.throwValue(err); break :brk null; }, } unreachable; }) orelse return null; var response = getAllocator(globalThis).create(Response) catch unreachable; response.* = Response{ .body = body, .allocator = getAllocator(globalThis), .url = "", }; if (response.body.value == .Blob and response.body.init.headers != null and response.body.value.Blob.content_type.len > 0 and !response.body.init.headers.?.fastHas(.ContentType)) { response.body.init.headers.?.put("content-type", response.body.value.Blob.content_type, globalThis); } return response; } }; const null_fd = bun.invalid_fd; pub const Fetch = struct { const headers_string = "headers"; const method_string = "method"; const JSType = js.JSType; pub const fetch_error_no_args = "fetch() expects a string but received no arguments."; pub const fetch_error_blank_url = "fetch() URL must not be a blank string."; pub const fetch_error_unexpected_body = "fetch() request with GET/HEAD/OPTIONS method cannot have body."; const JSTypeErrorEnum = std.enums.EnumArray(JSType, string); pub const fetch_type_error_names: JSTypeErrorEnum = brk: { var errors = JSTypeErrorEnum.initUndefined(); errors.set(JSType.kJSTypeUndefined, "Undefined"); errors.set(JSType.kJSTypeNull, "Null"); errors.set(JSType.kJSTypeBoolean, "Boolean"); errors.set(JSType.kJSTypeNumber, "Number"); errors.set(JSType.kJSTypeString, "String"); errors.set(JSType.kJSTypeObject, "Object"); errors.set(JSType.kJSTypeSymbol, "Symbol"); break :brk errors; }; pub const fetch_type_error_string_values = .{ std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeUndefined)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNull)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeBoolean)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNumber)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeString)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeObject)}), std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeSymbol)}), }; pub const fetch_type_error_strings: JSTypeErrorEnum = brk: { var errors = JSTypeErrorEnum.initUndefined(); errors.set( JSType.kJSTypeUndefined, bun.asByteSlice(fetch_type_error_string_values[0]), ); errors.set( JSType.kJSTypeNull, bun.asByteSlice(fetch_type_error_string_values[1]), ); errors.set( JSType.kJSTypeBoolean, bun.asByteSlice(fetch_type_error_string_values[2]), ); errors.set( JSType.kJSTypeNumber, bun.asByteSlice(fetch_type_error_string_values[3]), ); errors.set( JSType.kJSTypeString, bun.asByteSlice(fetch_type_error_string_values[4]), ); errors.set( JSType.kJSTypeObject, bun.asByteSlice(fetch_type_error_string_values[5]), ); errors.set( JSType.kJSTypeSymbol, bun.asByteSlice(fetch_type_error_string_values[6]), ); break :brk errors; }; pub const Class = NewClass( void, .{ .name = "fetch" }, .{ .call = .{ .rfn = Fetch.call, .ts = d.ts{}, }, }, .{}, ); pub const FetchTasklet = struct { const log = Output.scoped(.FetchTasklet, false); http: ?*HTTPClient.AsyncHTTP = null, result: HTTPClient.HTTPClientResult = .{}, javascript_vm: *VirtualMachine = undefined, global_this: *JSGlobalObject = undefined, request_body: AnyBlob = undefined, response_buffer: MutableString = undefined, request_headers: Headers = Headers{ .allocator = undefined }, ref: *JSC.napi.Ref = undefined, concurrent_task: JSC.ConcurrentTask = .{}, poll_ref: JSC.PollRef = .{}, /// This is url + proxy memory buffer and is owned by FetchTasklet /// We always clone url and proxy (if informed) url_proxy_buffer: []const u8 = "", signal: ?*JSC.WebCore.AbortSignal = null, aborted: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false), // must be stored because AbortSignal stores reason weakly abort_reason: JSValue = JSValue.zero, pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { return FetchTasklet{}; } fn clearData(this: *FetchTasklet) void { if (this.url_proxy_buffer.len > 0) { bun.default_allocator.free(this.url_proxy_buffer); this.url_proxy_buffer.len = 0; } this.request_headers.entries.deinit(bun.default_allocator); this.request_headers.buf.deinit(bun.default_allocator); this.request_headers = Headers{ .allocator = undefined }; this.http.?.deinit(); this.result.deinitMetadata(); this.response_buffer.deinit(); this.request_body.detach(); if (this.abort_reason != .zero) this.abort_reason.unprotect(); if (this.signal) |signal| { signal.cleanNativeBindings(this); _ = signal.unref(); this.signal = null; } } pub fn deinit(this: *FetchTasklet) void { if (this.http) |http| this.javascript_vm.allocator.destroy(http); this.javascript_vm.allocator.destroy(this); } pub fn onDone(this: *FetchTasklet) void { JSC.markBinding(@src()); const globalThis = this.global_this; var ref = this.ref; const promise_value = ref.get(); defer ref.destroy(); var poll_ref = this.poll_ref; var vm = globalThis.bunVM(); defer poll_ref.unref(vm); if (promise_value.isEmptyOrUndefinedOrNull()) { this.clearData(); return; } const promise = promise_value.asAnyPromise().?; const success = this.result.isSuccess(); const result = switch (success) { true => this.onResolve(), false => this.onReject(), }; result.ensureStillAlive(); this.clearData(); promise_value.ensureStillAlive(); switch (success) { true => { promise.resolve(globalThis, result); }, false => { promise.reject(globalThis, result); }, } } pub fn onReject(this: *FetchTasklet) JSValue { if (this.signal) |signal| { _ = signal.unref(); this.signal = null; } if (!this.abort_reason.isEmptyOrUndefinedOrNull()) { return this.abort_reason; } if (this.result.isTimeout()) { // Timeout without reason return JSC.WebCore.AbortSignal.createTimeoutError(JSC.ZigString.static("The operation timed out"), &JSC.ZigString.Empty, this.global_this); } if (this.result.isAbort()) { // Abort without reason return JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.global_this); } const fetch_error = JSC.SystemError{ .code = ZigString.init(@errorName(this.result.fail)), .message = switch (this.result.fail) { error.ConnectionClosed => ZigString.init("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), error.FailedToOpenSocket => ZigString.init("Was there a typo in the url or port?"), error.TooManyRedirects => ZigString.init("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), error.ConnectionRefused => ZigString.init("Unable to connect. Is the computer able to access the url?"), else => ZigString.init("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), }, .path = ZigString.init(this.http.?.url.href), }; return fetch_error.toErrorInstance(this.global_this); } fn toBodyValue(this: *FetchTasklet) Body.Value { var response_buffer = this.response_buffer.list; const response = Body.Value{ .InternalBlob = .{ .bytes = response_buffer.toManaged(bun.default_allocator), }, }; this.response_buffer = .{ .allocator = default_allocator, .list = .{ .items = &.{}, .capacity = 0, }, }; // if (response_buffer.items.len < InlineBlob.available_bytes) { // const inline_blob = InlineBlob.init(response_buffer.items); // defer response_buffer.deinit(bun.default_allocator); // return .{ .InlineBlob = inline_blob }; // } return response; } fn toResponse(this: *FetchTasklet, allocator: std.mem.Allocator) Response { const http_response = this.result.response; return Response{ .allocator = allocator, .url = allocator.dupe(u8, this.result.href) catch unreachable, .status_text = allocator.dupe(u8, http_response.status) catch unreachable, .redirected = this.result.redirected, .body = .{ .init = .{ .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), .status_code = @truncate(u16, http_response.status_code), }, .value = this.toBodyValue(), }, }; } pub fn onResolve(this: *FetchTasklet) JSValue { var allocator = this.global_this.bunVM().allocator; var response = allocator.create(Response) catch unreachable; response.* = this.toResponse(allocator); return Response.makeMaybePooled(@ptrCast(js.JSContextRef, this.global_this), response); } pub fn get( allocator: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, promise: JSValue, fetch_options: FetchOptions, ) !*FetchTasklet { var jsc_vm = globalThis.bunVM(); var fetch_tasklet = try jsc_vm.allocator.create(FetchTasklet); fetch_tasklet.* = .{ .response_buffer = MutableString{ .allocator = bun.default_allocator, .list = .{ .items = &.{}, .capacity = 0, }, }, .http = try jsc_vm.allocator.create(HTTPClient.AsyncHTTP), .javascript_vm = jsc_vm, .request_body = fetch_options.body, .global_this = globalThis, .request_headers = fetch_options.headers, .ref = JSC.napi.Ref.create(globalThis, promise), .url_proxy_buffer = fetch_options.url_proxy_buffer, .signal = fetch_options.signal, }; if (fetch_tasklet.request_body.store()) |store| { store.ref(); } var proxy: ?ZigURL = null; if (fetch_options.proxy) |proxy_opt| { if (!proxy_opt.isEmpty()) { //if is empty just ignore proxy proxy = fetch_options.proxy orelse jsc_vm.bundler.env.getHttpProxy(fetch_options.url); } } else { proxy = jsc_vm.bundler.env.getHttpProxy(fetch_options.url); } fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init(allocator, fetch_options.method, fetch_options.url, fetch_options.headers.entries, fetch_options.headers.buf.items, &fetch_tasklet.response_buffer, fetch_tasklet.request_body.slice(), fetch_options.timeout, HTTPClient.HTTPClientResult.Callback.New( *FetchTasklet, FetchTasklet.callback, ).init( fetch_tasklet, ), proxy, if (fetch_tasklet.signal != null) &fetch_tasklet.aborted else null); if (!fetch_options.follow_redirects) { fetch_tasklet.http.?.client.remaining_redirect_count = 0; } fetch_tasklet.http.?.client.disable_timeout = fetch_options.disable_timeout; fetch_tasklet.http.?.client.verbose = fetch_options.verbose; fetch_tasklet.http.?.client.disable_keepalive = fetch_options.disable_keepalive; if (fetch_tasklet.signal) |signal| { fetch_tasklet.signal = signal.listen(FetchTasklet, fetch_tasklet, FetchTasklet.abortListener); } return fetch_tasklet; } pub fn abortListener(this: *FetchTasklet, reason: JSValue) void { log("abortListener", .{}); reason.ensureStillAlive(); this.abort_reason = reason; reason.protect(); this.aborted.store(true, .Monotonic); if (this.http != null) { HTTPClient.http_thread.scheduleShutdown(this.http.?); } } const FetchOptions = struct { method: Method, headers: Headers, body: AnyBlob, timeout: usize, disable_timeout: bool, disable_keepalive: bool, url: ZigURL, verbose: bool = false, follow_redirects: bool = true, proxy: ?ZigURL = null, url_proxy_buffer: []const u8 = "", signal: ?*JSC.WebCore.AbortSignal = null, globalThis: ?*JSGlobalObject, }; pub fn queue( allocator: std.mem.Allocator, global: *JSGlobalObject, fetch_options: FetchOptions, promise: JSValue, ) !*FetchTasklet { try HTTPClient.HTTPThread.init(); var node = try get( allocator, global, promise, fetch_options, ); var batch = NetworkThread.Batch{}; node.http.?.schedule(allocator, &batch); node.poll_ref.ref(global.bunVM()); HTTPClient.http_thread.schedule(batch); return node; } pub fn callback(task: *FetchTasklet, result: HTTPClient.HTTPClientResult) void { task.response_buffer = result.body.?.*; task.result = result; task.javascript_vm.eventLoop().enqueueTaskConcurrent(task.concurrent_task.from(task)); } }; pub fn call( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { var globalThis = ctx.ptr(); if (arguments.len == 0) { const err = JSC.toTypeError(.ERR_MISSING_ARGS, fetch_error_no_args, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } var headers: ?Headers = null; var method = Method.GET; var args = JSC.Node.ArgumentsSlice.from(ctx.bunVM(), arguments); defer args.deinit(); var url: ZigURL = undefined; var first_arg = args.nextEat().?; var body: AnyBlob = AnyBlob{ .Blob = .{}, }; var disable_timeout = false; var disable_keepalive = false; var verbose = false; var proxy: ?ZigURL = null; var follow_redirects = true; var signal: ?*JSC.WebCore.AbortSignal = null; var url_proxy_buffer: []const u8 = undefined; if (first_arg.as(Request)) |request| { if (arguments.len >= 2) { const options = arguments[1].?.value(); if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx)); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } else { method = request.method; } if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { headers = Headers.from(headers__, bun.default_allocator) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { headers = Headers.from(headers__, bun.default_allocator) catch unreachable; headers__.deref(); } else if (request.headers) |head| { headers = Headers.from(head, bun.default_allocator) catch unreachable; } } else if (request.headers) |head| { headers = Headers.from(head, bun.default_allocator) catch unreachable; } if (options.fastGet(ctx.ptr(), .body)) |body__| { if (Body.Value.fromJS(ctx.ptr(), body__)) |body_const| { var body_value = body_const; // TODO: buffer ReadableStream? // we have to explicitly check for InternalBlob body = body_value.useAsAnyBlob(); } else { // an error was thrown return JSC.JSValue.jsUndefined().asObjectRef(); } } else { body = request.body.useAsAnyBlob(); } if (options.get(ctx, "timeout")) |timeout_value| { if (timeout_value.isBoolean()) { disable_timeout = !timeout_value.asBoolean(); } else if (timeout_value.isNumber()) { disable_timeout = timeout_value.to(i32) == 0; } } if (options.get(ctx, "redirect")) |redirect_value| { if (redirect_value.getZigString(globalThis).eqlComptime("manual")) { follow_redirects = false; } } if (options.get(ctx, "keepalive")) |keepalive_value| { if (keepalive_value.isBoolean()) { disable_keepalive = !keepalive_value.asBoolean(); } else if (keepalive_value.isNumber()) { disable_keepalive = keepalive_value.to(i32) == 0; } } if (options.get(globalThis, "verbose")) |verb| { verbose = verb.toBoolean(); } if (options.get(globalThis, "signal")) |signal_arg| { if (signal_arg.as(JSC.WebCore.AbortSignal)) |signal_| { _ = signal_.ref(); signal = signal_; } } if (options.get(globalThis, "proxy")) |proxy_arg| { if (!proxy_arg.isUndefined()) { if (proxy_arg.isNull()) { //if null we add an empty proxy to be ignore all proxy //only allocate url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; proxy = ZigURL{}; //empty proxy } else { var proxy_str = proxy_arg.toStringOrNull(globalThis) orelse return null; // proxy + url 1 allocation var proxy_url_zig = proxy_str.getZigString(globalThis); // ignore proxy if it is len = 0 if (proxy_url_zig.len == 0) { url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; } else { var buffer = getAllocator(ctx).alloc(u8, request.url.len + proxy_url_zig.len) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; @memcpy(buffer.ptr, request.url.ptr, request.url.len); var proxy_url_slice = buffer[request.url.len..]; @memcpy(proxy_url_slice.ptr, proxy_url_zig.ptr, proxy_url_zig.len); url = ZigURL.parse(buffer[0..request.url.len]); proxy = ZigURL.parse(proxy_url_slice); url_proxy_buffer = buffer; } } } } else { // no proxy only url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; } } } else { method = request.method; if (request.headers) |head| { headers = Headers.from(head, bun.default_allocator) catch unreachable; } body = request.body.useAsAnyBlob(); // no proxy only url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; if (request.signal) |signal_| { _ = signal_.ref(); signal = signal_; } } } else if (first_arg.toStringOrNull(globalThis)) |jsstring| { if (arguments.len >= 2) { const options = arguments[1].?.value(); if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { var slice_ = method_.toSlice(ctx.ptr(), getAllocator(ctx)); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { headers = Headers.from(headers__, bun.default_allocator) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { defer headers__.deref(); headers = Headers.from(headers__, bun.default_allocator) catch unreachable; } else { // Converting the headers failed; return null and // let the set exception get thrown return null; } } if (options.fastGet(ctx.ptr(), .body)) |body__| { if (Body.Value.fromJS(ctx.ptr(), body__)) |body_const| { var body_value = body_const; // TODO: buffer ReadableStream? // we have to explicitly check for InternalBlob body = body_value.useAsAnyBlob(); } else { // an error was thrown return JSC.JSValue.jsUndefined().asObjectRef(); } } if (options.get(ctx, "timeout")) |timeout_value| { if (timeout_value.isBoolean()) { disable_timeout = !timeout_value.asBoolean(); } else if (timeout_value.isNumber()) { disable_timeout = timeout_value.to(i32) == 0; } } if (options.get(ctx, "redirect")) |redirect_value| { if (redirect_value.getZigString(globalThis).eqlComptime("manual")) { follow_redirects = false; } } if (options.get(ctx, "keepalive")) |keepalive_value| { if (keepalive_value.isBoolean()) { disable_keepalive = !keepalive_value.asBoolean(); } else if (keepalive_value.isNumber()) { disable_keepalive = keepalive_value.to(i32) == 0; } } if (options.get(globalThis, "verbose")) |verb| { verbose = verb.toBoolean(); } if (options.get(globalThis, "signal")) |signal_arg| { if (signal_arg.as(JSC.WebCore.AbortSignal)) |signal_| { _ = signal_.ref(); signal = signal_; } } if (options.get(globalThis, "proxy")) |proxy_arg| { if (!proxy_arg.isUndefined()) { // proxy + url 1 allocation var url_zig = jsstring.getZigString(globalThis); if (url_zig.len == 0) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } if (proxy_arg.isNull()) { //if null we add an empty proxy to be ignore all proxy //only allocate url const url_slice = url_zig.toSlice(bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; url = ZigURL.parse(url_slice.slice()); url_proxy_buffer = url.href; proxy = ZigURL{}; //empty proxy } else { var proxy_str = proxy_arg.toStringOrNull(globalThis) orelse return null; var proxy_url_zig = proxy_str.getZigString(globalThis); // proxy is actual 0 len so ignores it if (proxy_url_zig.len == 0) { const url_slice = url_zig.toSlice(bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; url = ZigURL.parse(url_slice.slice()); url_proxy_buffer = url.href; } else { var buffer = getAllocator(ctx).alloc(u8, url_zig.len + proxy_url_zig.len) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; @memcpy(buffer.ptr, url_zig.ptr, url_zig.len); var proxy_url_slice = buffer[url_zig.len..]; @memcpy(proxy_url_slice.ptr, proxy_url_zig.ptr, proxy_url_zig.len); url = ZigURL.parse(buffer[0..url_zig.len]); proxy = ZigURL.parse(proxy_url_slice); url_proxy_buffer = buffer; } } } else { //no proxy only url var url_slice = jsstring.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; if (url_slice.len == 0) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } url = ZigURL.parse(url_slice.slice()); url_proxy_buffer = url.href; } } else { //no proxy only url var url_slice = jsstring.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; if (url_slice.len == 0) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } url = ZigURL.parse(url_slice.slice()); url_proxy_buffer = url.href; } } } else { //no proxy only url var url_slice = jsstring.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch { JSC.JSError(bun.default_allocator, "Out of memory", .{}, ctx, exception); return null; }; if (url_slice.len == 0) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } url = ZigURL.parse(url_slice.slice()); url_proxy_buffer = url.href; } } else { const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, arguments[0])); const err = JSC.toTypeError(.ERR_INVALID_ARG_TYPE, "{s}", .{fetch_error}, ctx); exception.* = err.asObjectRef(); return null; } var promise = JSPromise.Strong.init(globalThis); var promise_val = promise.value(); if (!method.hasRequestBody() and body.size() > 0) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err).asRef(); } // var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType) _ = FetchTasklet.queue( default_allocator, globalThis, .{ .method = method, .url = url, .headers = headers orelse Headers{ .allocator = bun.default_allocator, }, .body = body, .timeout = std.time.ns_per_hour, .disable_keepalive = disable_keepalive, .disable_timeout = disable_timeout, .follow_redirects = follow_redirects, .verbose = verbose, .proxy = proxy, .url_proxy_buffer = url_proxy_buffer, .signal = signal, .globalThis = globalThis, }, promise_val, ) catch unreachable; return promise_val.asRef(); } }; // https://developer.mozilla.org/en-US/docs/Web/API/Headers pub const Headers = struct { pub usingnamespace HTTPClient.Headers; entries: Headers.Entries = .{}, buf: std.ArrayListUnmanaged(u8) = .{}, allocator: std.mem.Allocator, pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 { return if (ptr.offset + ptr.length <= this.buf.items.len) this.buf.items[ptr.offset..][0..ptr.length] else ""; } pub fn from(headers_ref: *FetchHeaders, allocator: std.mem.Allocator) !Headers { var header_count: u32 = 0; var buf_len: u32 = 0; headers_ref.count(&header_count, &buf_len); var headers = Headers{ .entries = .{}, .buf = .{}, .allocator = allocator, }; headers.entries.ensureTotalCapacity(allocator, header_count) catch unreachable; headers.entries.len = header_count; headers.buf.ensureTotalCapacityPrecise(allocator, buf_len) catch unreachable; headers.buf.items.len = buf_len; var sliced = headers.entries.slice(); var names = sliced.items(.name); var values = sliced.items(.value); headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr); return headers; } }; // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/FetchEvent.h pub const FetchEvent = struct { started_waiting_at: u64 = 0, response: ?*Response = null, request_context: ?*RequestContext = null, request: Request, pending_promise: JSValue = JSValue.zero, onPromiseRejectionCtx: *anyopaque = undefined, onPromiseRejectionHandler: ?*const fn (ctx: *anyopaque, err: anyerror, fetch_event: *FetchEvent, value: JSValue) void = null, rejected: bool = false, pub const Class = NewClass( FetchEvent, .{ .name = "FetchEvent", .read_only = true, .ts = .{ .class = d.ts.class{ .interface = true } }, }, .{ .respondWith = .{ .rfn = respondWith, .ts = d.ts{ .tsdoc = "Render the response in the active HTTP request", .@"return" = "void", .args = &[_]d.ts.arg{ .{ .name = "response", .@"return" = "Response" }, }, }, }, .waitUntil = waitUntil, .finalize = finalize, }, .{ .client = .{ .get = getClient, .ro = true, .ts = d.ts{ .tsdoc = "HTTP client metadata. This is not implemented yet, do not use.", .@"return" = "undefined", }, }, .request = .{ .get = getRequest, .ro = true, .ts = d.ts{ .tsdoc = "HTTP request", .@"return" = "InstanceType", }, }, }, ); pub fn finalize( this: *FetchEvent, ) void { VirtualMachine.get().allocator.destroy(this); } pub fn getClient( _: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { Output.prettyErrorln("FetchEvent.client is not implemented yet - sorry!!", .{}); Output.flush(); return js.JSValueMakeUndefined(ctx); } pub fn getRequest( this: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { var req = bun.default_allocator.create(Request) catch unreachable; req.* = this.request; return req.toJS( ctx, ).asObjectRef(); } // https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith pub fn respondWith( this: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSValueRef { var request_context = this.request_context orelse return js.JSValueMakeUndefined(ctx); if (request_context.has_called_done) return js.JSValueMakeUndefined(ctx); var globalThis = ctx.ptr(); // A Response or a Promise that resolves to a Response. Otherwise, a network error is returned to Fetch. if (arguments.len == 0) { JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise.", .{}, ctx, exception); request_context.sendInternalError(error.respondWithWasEmpty) catch {}; return js.JSValueMakeUndefined(ctx); } var arg = arguments[0]; var existing_response: ?*Response = arguments[0].?.value().as(Response); if (existing_response == null) { switch (JSValue.fromRef(arg).jsType()) { .JSPromise => { this.pending_promise = JSValue.fromRef(arg); }, else => { JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise.", .{}, ctx, exception); request_context.sendInternalError(error.respondWithWasNotResponse) catch {}; return js.JSValueMakeUndefined(ctx); }, } } if (this.pending_promise.asAnyPromise()) |promise| { globalThis.bunVM().waitForPromise(promise); switch (promise.status(ctx.ptr().vm())) { .Fulfilled => {}, else => { this.rejected = true; this.pending_promise = JSValue.zero; this.onPromiseRejectionHandler.?( this.onPromiseRejectionCtx, error.PromiseRejection, this, promise.result(globalThis.vm()), ); return js.JSValueMakeUndefined(ctx); }, } arg = promise.result(ctx.ptr().vm()).asRef(); } var response: *Response = JSValue.c(arg.?).as(Response) orelse { this.rejected = true; this.pending_promise = JSValue.zero; JSError(getAllocator(ctx), "event.respondWith() expects Response or Promise", .{}, ctx, exception); this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidTypeInternal, this, JSValue.fromRef(exception.*)); return js.JSValueMakeUndefined(ctx); }; defer { if (!VirtualMachine.get().had_errors) { Output.printElapsed(@intToFloat(f64, (request_context.timer.lap())) / std.time.ns_per_ms); Output.prettyError( " {s} - {d} transpiled, {d} imports\n", .{ request_context.matched_route.?.name, VirtualMachine.get().transpiled_count, VirtualMachine.get().resolved_count, }, ); } } defer this.pending_promise = JSValue.zero; var needs_mime_type = true; var content_length: ?usize = null; if (response.body.init.headers) |headers_ref| { var headers = Headers.from(headers_ref, request_context.allocator) catch unreachable; var i: usize = 0; while (i < headers.entries.len) : (i += 1) { var header = headers.entries.get(i); const name = headers.asStr(header.name); if (strings.eqlComptime(name, "content-type") and headers.asStr(header.value).len > 0) { needs_mime_type = false; } if (strings.eqlComptime(name, "content-length")) { content_length = std.fmt.parseInt(usize, headers.asStr(header.value), 10) catch null; continue; } // Some headers need to be managed by bun if (strings.eqlComptime(name, "transfer-encoding") or strings.eqlComptime(name, "content-encoding") or strings.eqlComptime(name, "strict-transport-security") or strings.eqlComptime(name, "content-security-policy")) { continue; } request_context.appendHeaderSlow( name, headers.asStr(header.value), ) catch unreachable; } } if (needs_mime_type) { request_context.appendHeader("Content-Type", response.mimeTypeWithDefault(MimeType.html, request_context)); } var blob = response.body.value.use(); defer blob.deinit(); const content_length_ = content_length orelse blob.size; if (content_length_ == 0) { request_context.sendNoContent() catch return js.JSValueMakeUndefined(ctx); return js.JSValueMakeUndefined(ctx); } if (FeatureFlags.strong_etags_for_built_files) { const did_send = request_context.writeETag(blob.sharedView()) catch false; if (did_send) { // defer getAllocator(ctx).destroy(str.ptr); return js.JSValueMakeUndefined(ctx); } } defer request_context.done(); request_context.writeStatusSlow(response.body.init.status_code) catch return js.JSValueMakeUndefined(ctx); request_context.prepareToSendBody(content_length_, false) catch return js.JSValueMakeUndefined(ctx); request_context.writeBodyBuf(blob.sharedView()) catch return js.JSValueMakeUndefined(ctx); return js.JSValueMakeUndefined(ctx); } // our implementation of the event listener already does this // so this is a no-op for us pub fn waitUntil( _: *FetchEvent, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSValueRef { return js.JSValueMakeUndefined(ctx); } };