const std = @import("std"); const Api = @import("../../api/schema.zig").Api; const bun = @import("root").bun; const RequestContext = @import("../../bun_dev_http_server.zig").RequestContext; const MimeType = @import("../../bun_dev_http_server.zig").MimeType; const ZigURL = @import("../../url.zig").URL; const HTTPClient = @import("root").bun.HTTP; const FetchRedirect = HTTPClient.FetchRedirect; const NetworkThread = HTTPClient.NetworkThread; const AsyncIO = NetworkThread.AsyncIO; const JSC = @import("root").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("root").bun.Output; const MutableString = @import("root").bun.MutableString; const strings = @import("root").bun.strings; const string = @import("root").bun.string; const default_allocator = @import("root").bun.default_allocator; const FeatureFlags = @import("root").bun.FeatureFlags; const ArrayBuffer = @import("../base.zig").ArrayBuffer; const Properties = @import("../base.zig").Properties; const castObj = @import("../base.zig").castObj; const getAllocator = @import("../base.zig").getAllocator; 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 DataURL = @import("../../resolver/data_url.zig").DataURL; const VirtualMachine = JSC.VirtualMachine; const Task = JSC.Task; const JSPrinter = bun.js_printer; const picohttp = @import("root").bun.picohttp; const StringJoiner = @import("../../string_joiner.zig"); const uws = @import("root").bun.uws; const Mutex = @import("../../lock.zig").Lock; 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; const BoringSSL = bun.BoringSSL; const X509 = @import("../api/bun/x509.zig"); pub const Response = struct { const ResponseMixin = BodyMixin(@This()); pub usingnamespace JSC.Codegen.JSResponse; allocator: std.mem.Allocator, body: Body, url: bun.String = bun.String.empty, status_text: bun.String = bun.String.empty, 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 = @as( u63, @intCast(this.body.value.estimatedSize() + this.url.byteSlice().len + this.status_text.byteSlice().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: *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(comptime Output.prettyFmt("ok: ", enable_ansi_colors)); 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(comptime Output.prettyFmt("url: \"", enable_ansi_colors)); try writer.print(comptime Output.prettyFmt("{}", 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(comptime Output.prettyFmt("headers: ", enable_ansi_colors)); formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("statusText: ", enable_ansi_colors)); try writer.print(comptime Output.prettyFmt("\"{}\"", enable_ansi_colors), .{this.status_text}); formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("redirected: ", enable_ansi_colors)); 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 this.url.toJS(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 this.status_text.toJS(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); return Response.makeMaybePooled(globalThis, cloned); } 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 = this.url.clone(), .status_text = this.status_text.clone(), .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; this.status_text.deref(); this.url.deref(); allocator.destroy(this); } pub fn mimeType(response: *const Response, request_ctx_: ?*const RequestContext) string { if (comptime Environment.isWindows) unreachable; return mimeTypeWithDefault(response, MimeType.other, request_ctx_); } pub fn mimeTypeWithDefault(response: *const Response, default: MimeType, request_ctx_: ?*const RequestContext) string { if (comptime Environment.isWindows) unreachable; 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; }, .WTFStringImpl => |str| { if (bun.String.init(str).hasPrefixComptime("")) { return MimeType.html.value; } return default.value; }, .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 = bun.String.empty, }; const json_value = args.nextEat() orelse JSC.JSValue.zero; if (@intFromEnum(json_value) != 0) { var str = bun.String.empty; // calling JSON.stringify on an empty string adds extra quotes // so this is correct json_value.jsonStringify(globalThis, 0, &str); if (!str.isEmpty()) { if (str.value.WTFStringImpl.toUTF8IfNeeded(bun.default_allocator)) |bytes| { defer str.deref(); response.body.value = .{ .InternalBlob = InternalBlob{ .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, @constCast(bytes.slice())), .was_string = true, }, }; } else { response.body.value = Body.Value{ .WTFStringImpl = str.value.WTFStringImpl, }; } } } if (args.nextEat()) |init| { if (init.isUndefinedOrNull()) {} else if (init.isNumber()) { response.body.init.status_code = @as(u16, @intCast(@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 = bun.String.empty, }; const url_string_value = args.nextEat() orelse JSC.JSValue.zero; var url_string = ZigString.init(""); if (@intFromEnum(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 = @as(u16, @intCast(@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), }; 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), }; 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; }; comptime { if (!JSC.is_bindgen) { _ = Bun__fetch; } } pub const FetchTasklet = struct { const log = Output.scoped(.FetchTasklet, false); http: ?*HTTPClient.AsyncHTTP = null, result: HTTPClient.HTTPClientResult = .{}, metadata: ?HTTPClient.HTTPResponseMetadata = null, javascript_vm: *VirtualMachine = undefined, global_this: *JSGlobalObject = undefined, request_body: HTTPRequestBody = undefined, /// buffer being used by AsyncHTTP response_buffer: MutableString = undefined, /// buffer used to stream response to JS scheduled_response_buffer: MutableString = undefined, /// response strong ref response: JSC.Strong = .{}, /// stream strong ref if any is available readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{}, request_headers: Headers = Headers{ .allocator = undefined }, promise: JSC.JSPromise.Strong, concurrent_task: JSC.ConcurrentTask = .{}, poll_ref: JSC.PollRef = .{}, memory_reporter: *JSC.MemoryReportingAllocator, /// For Http Client requests /// when Content-Length is provided this represents the whole size of the request /// If chunked encoded this will represent the total received size (ignoring the chunk headers) /// If is not chunked encoded and Content-Length is not provided this will be unknown body_size: HTTPClient.HTTPClientResult.BodySize = .unknown, /// 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, signals: HTTPClient.Signals = .{}, signal_store: HTTPClient.Signals.Store = .{}, has_schedule_callback: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false), // must be stored because AbortSignal stores reason weakly abort_reason: JSValue = JSValue.zero, // custom checkServerIdentity check_server_identity: JSC.Strong = .{}, reject_unauthorized: bool = true, // Custom Hostname hostname: ?[]u8 = null, is_waiting_body: bool = false, is_waiting_abort: bool = false, mutex: Mutex, tracker: JSC.AsyncTaskTracker, pub const HTTPRequestBody = union(enum) { AnyBlob: AnyBlob, Sendfile: HTTPClient.Sendfile, pub fn store(this: *HTTPRequestBody) ?*JSC.WebCore.Blob.Store { return switch (this.*) { .AnyBlob => this.AnyBlob.store(), else => null, }; } pub fn slice(this: *const HTTPRequestBody) []const u8 { return switch (this.*) { .AnyBlob => this.AnyBlob.slice(), else => "", }; } pub fn detach(this: *HTTPRequestBody) void { switch (this.*) { .AnyBlob => this.AnyBlob.detach(), .Sendfile => { if (@max(this.Sendfile.offset, this.Sendfile.remain) > 0) _ = bun.sys.close(this.Sendfile.fd); this.Sendfile.offset = 0; this.Sendfile.remain = 0; }, } } }; pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { return FetchTasklet{}; } fn clearData(this: *FetchTasklet) void { log("clearData", .{}); const allocator = this.memory_reporter.allocator(); if (this.url_proxy_buffer.len > 0) { allocator.free(this.url_proxy_buffer); this.url_proxy_buffer.len = 0; } if (this.hostname) |hostname| { allocator.free(hostname); this.hostname = null; } this.request_headers.entries.deinit(allocator); this.request_headers.buf.deinit(allocator); this.request_headers = Headers{ .allocator = undefined }; if (this.http != null) { this.http.?.clearData(); } if (this.metadata != null) { this.metadata.?.deinit(allocator); this.metadata = null; } this.response_buffer.deinit(); this.response.deinit(); this.readable_stream_ref.deinit(); this.scheduled_response_buffer.deinit(); this.request_body.detach(); if (this.abort_reason != .zero) this.abort_reason.unprotect(); this.check_server_identity.deinit(); if (this.signal) |signal| { this.signal = null; signal.detach(this); } } pub fn deinit(this: *FetchTasklet) void { log("deinit", .{}); var reporter = this.memory_reporter; const allocator = reporter.allocator(); if (this.http) |http| allocator.destroy(http); allocator.destroy(this); // reporter.assert(); bun.default_allocator.destroy(reporter); } pub fn onBodyReceived(this: *FetchTasklet) void { this.mutex.lock(); const success = this.result.isSuccess(); const globalThis = this.global_this; const is_done = !success or !this.result.has_more; defer { this.has_schedule_callback.store(false, .Monotonic); this.mutex.unlock(); if (is_done) { var vm = globalThis.bunVM(); this.poll_ref.unref(vm); this.clearData(); this.deinit(); } } if (!success) { const err = this.onReject(); err.ensureStillAlive(); // if we are streaming update with error if (this.readable_stream_ref.get()) |readable| { readable.ptr.Bytes.onData( .{ .err = .{ .JSValue = err }, }, bun.default_allocator, ); } // if we are buffering resolve the promise if (this.response.get()) |response_js| { if (response_js.as(Response)) |response| { const body = response.body; if (body.value.Locked.promise) |promise_| { const promise = promise_.asAnyPromise().?; promise.reject(globalThis, err); } response.body.value.toErrorInstance(err, globalThis); } } return; } if (this.readable_stream_ref.get()) |readable| { if (readable.ptr == .Bytes) { readable.ptr.Bytes.size_hint = this.getSizeHint(); // body can be marked as used but we still need to pipe the data var scheduled_response_buffer = this.scheduled_response_buffer.list; const chunk = scheduled_response_buffer.items; if (this.result.has_more) { readable.ptr.Bytes.onData( .{ .temporary = bun.ByteList.initConst(chunk), }, bun.default_allocator, ); // clean for reuse later this.scheduled_response_buffer.reset(); } else { readable.ptr.Bytes.onData( .{ .temporary_and_done = bun.ByteList.initConst(chunk), }, bun.default_allocator, ); } return; } } if (this.response.get()) |response_js| { if (response_js.as(Response)) |response| { const body = response.body; if (body.value == .Locked) { if (body.value.Locked.readable) |readable| { if (readable.ptr == .Bytes) { readable.ptr.Bytes.size_hint = this.getSizeHint(); var scheduled_response_buffer = this.scheduled_response_buffer.list; const chunk = scheduled_response_buffer.items; if (this.result.has_more) { readable.ptr.Bytes.onData( .{ .temporary = bun.ByteList.initConst(chunk), }, bun.default_allocator, ); // clean for reuse later this.scheduled_response_buffer.reset(); } else { readable.ptr.Bytes.onData( .{ .temporary_and_done = bun.ByteList.initConst(chunk), }, bun.default_allocator, ); } return; } } else { response.body.value.Locked.size_hint = this.getSizeHint(); } // we will reach here when not streaming if (!this.result.has_more) { var scheduled_response_buffer = this.scheduled_response_buffer.list; this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); // done resolve body var old = body.value; var body_value = Body.Value{ .InternalBlob = .{ .bytes = scheduled_response_buffer.toManaged(bun.default_allocator), }, }; response.body.value = body_value; this.scheduled_response_buffer = .{ .allocator = this.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }; if (old == .Locked) { old.resolve(&response.body.value, this.global_this); } } } } } } pub fn onProgressUpdate(this: *FetchTasklet) void { JSC.markBinding(@src()); log("onProgressUpdate", .{}); if (this.is_waiting_body) { return this.onBodyReceived(); } // if we abort because of cert error // we wait the Http Client because we already have the response // we just need to deinit const globalThis = this.global_this; this.mutex.lock(); if (this.is_waiting_abort) { // has_more will be false when the request is aborted/finished if (this.result.has_more) { this.mutex.unlock(); return; } this.mutex.unlock(); var poll_ref = this.poll_ref; var vm = globalThis.bunVM(); poll_ref.unref(vm); this.clearData(); this.deinit(); return; } var ref = this.promise; const promise_value = ref.valueOrEmpty(); var poll_ref = this.poll_ref; var vm = globalThis.bunVM(); if (promise_value.isEmptyOrUndefinedOrNull()) { log("onProgressUpdate: promise_value is null", .{}); ref.strong.deinit(); this.has_schedule_callback.store(false, .Monotonic); this.mutex.unlock(); poll_ref.unref(vm); this.clearData(); this.deinit(); return; } if (this.result.certificate_info) |certificate_info| { this.result.certificate_info = null; defer certificate_info.deinit(bun.default_allocator); // we receive some error if (this.reject_unauthorized and !this.checkServerIdentity(certificate_info)) { log("onProgressUpdate: aborted due certError", .{}); // we need to abort the request const promise = promise_value.asAnyPromise().?; const tracker = this.tracker; const result = this.onReject(); result.ensureStillAlive(); promise_value.ensureStillAlive(); promise.reject(globalThis, result); tracker.didDispatch(globalThis); ref.strong.deinit(); this.has_schedule_callback.store(false, .Monotonic); this.mutex.unlock(); if (this.is_waiting_abort) { return; } // we are already done we can deinit poll_ref.unref(vm); this.clearData(); this.deinit(); return; } // everything ok if (this.metadata == null) { log("onProgressUpdate: metadata is null", .{}); this.has_schedule_callback.store(false, .Monotonic); // cannot continue without metadata this.mutex.unlock(); return; } } const promise = promise_value.asAnyPromise().?; _ = promise; const tracker = this.tracker; tracker.willDispatch(globalThis); defer { log("onProgressUpdate: promise_value is not null", .{}); tracker.didDispatch(globalThis); ref.strong.deinit(); this.has_schedule_callback.store(false, .Monotonic); this.mutex.unlock(); if (!this.is_waiting_body) { poll_ref.unref(vm); this.clearData(); this.deinit(); } } const success = this.result.isSuccess(); const result = switch (success) { true => this.onResolve(), false => this.onReject(), }; result.ensureStillAlive(); promise_value.ensureStillAlive(); const Holder = struct { held: JSC.Strong, promise: JSC.Strong, globalObject: *JSC.JSGlobalObject, task: JSC.AnyTask, pub fn resolve(held: *@This()) void { var prom = held.promise.swap().asAnyPromise().?; var globalObject = held.globalObject; const res = held.held.swap(); held.held.deinit(); held.promise.deinit(); res.ensureStillAlive(); bun.default_allocator.destroy(held); prom.resolve(globalObject, res); } pub fn reject(held: *@This()) void { var prom = held.promise.swap().asAnyPromise().?; var globalObject = held.globalObject; const res = held.held.swap(); held.held.deinit(); held.promise.deinit(); res.ensureStillAlive(); bun.default_allocator.destroy(held); prom.reject(globalObject, res); } }; var holder = bun.default_allocator.create(Holder) catch unreachable; holder.* = .{ .held = JSC.Strong.create(result, globalThis), .promise = ref.strong, .globalObject = globalThis, .task = undefined, }; ref.strong = .{}; holder.task = switch (success) { true => JSC.AnyTask.New(Holder, Holder.resolve).init(holder), false => JSC.AnyTask.New(Holder, Holder.reject).init(holder), }; globalThis.bunVM().enqueueTask(JSC.Task.init(&holder.task)); } pub fn checkServerIdentity(this: *FetchTasklet, certificate_info: HTTPClient.CertificateInfo) bool { if (this.check_server_identity.get()) |check_server_identity| { check_server_identity.ensureStillAlive(); if (certificate_info.cert.len > 0) { var cert = certificate_info.cert; var cert_ptr = cert.ptr; if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| { defer BoringSSL.X509_free(x509); const globalObject = this.global_this; const js_cert = X509.toJS(x509, globalObject); var hostname: bun.String = bun.String.create(certificate_info.hostname); const js_hostname = hostname.toJS(globalObject); js_hostname.ensureStillAlive(); js_cert.ensureStillAlive(); const check_result = check_server_identity.callWithThis(globalObject, JSC.JSValue.jsUndefined(), &[_]JSC.JSValue{ js_hostname, js_cert }); // if check failed abort the request if (check_result.isAnyError()) { // mark to wait until deinit this.is_waiting_abort = this.result.has_more; check_result.ensureStillAlive(); check_result.protect(); this.abort_reason = check_result; this.signal_store.aborted.store(true, .Monotonic); this.tracker.didCancel(this.global_this); // we need to abort the request if (this.http != null) { HTTPClient.http_thread.scheduleShutdown(this.http.?); } this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; return false; } return true; } } } this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; return false; } pub fn onReject(this: *FetchTasklet) JSValue { log("onReject", .{}); if (this.signal) |signal| { this.signal = null; signal.detach(this); } 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); } var path: bun.String = undefined; // some times we don't have metadata so we also check http.url if (this.metadata) |metadata| { path = bun.String.create(metadata.url); } else if (this.http) |http| { path = bun.String.create(http.url.href); } else { path = bun.String.empty; } const fetch_error = JSC.SystemError{ .code = bun.String.static(@errorName(this.result.fail)), .message = switch (this.result.fail) { error.ConnectionClosed => bun.String.static("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), error.FailedToOpenSocket => bun.String.static("Was there a typo in the url or port?"), error.TooManyRedirects => bun.String.static("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), error.ConnectionRefused => bun.String.static("Unable to connect. Is the computer able to access the url?"), else => bun.String.static("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), }, .path = path, }; return fetch_error.toErrorInstance(this.global_this); } pub fn onReadableStreamAvailable(ctx: *anyopaque, readable: JSC.WebCore.ReadableStream) void { const this = bun.cast(*FetchTasklet, ctx); this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, this.global_this) catch .{}; } pub fn onStartStreamingRequestBodyCallback(ctx: *anyopaque) JSC.WebCore.DrainResult { const this = bun.cast(*FetchTasklet, ctx); if (this.http) |http| { http.enableBodyStreaming(); } if (this.signal_store.aborted.load(.Monotonic)) { return JSC.WebCore.DrainResult{ .aborted = {}, }; } this.mutex.lock(); defer this.mutex.unlock(); const size_hint = this.getSizeHint(); var scheduled_response_buffer = this.scheduled_response_buffer.list; // This means we have received part of the body but not the whole thing if (scheduled_response_buffer.items.len > 0) { this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); this.scheduled_response_buffer = .{ .allocator = this.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }; return .{ .owned = .{ .list = scheduled_response_buffer.toManaged(bun.default_allocator), .size_hint = size_hint, }, }; } return .{ .estimated_size = size_hint, }; } fn getSizeHint(this: *FetchTasklet) Blob.SizeType { return switch (this.body_size) { .content_length => @truncate(this.body_size.content_length), .total_received => @truncate(this.body_size.total_received), else => 0, }; } fn toBodyValue(this: *FetchTasklet) Body.Value { if (this.is_waiting_body) { const response = Body.Value{ .Locked = .{ .size_hint = this.getSizeHint(), .task = this, .global = this.global_this, .onStartStreaming = FetchTasklet.onStartStreamingRequestBodyCallback, .onReadableStreamAvailable = FetchTasklet.onReadableStreamAvailable, }, }; return response; } var scheduled_response_buffer = this.scheduled_response_buffer.list; this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); const response = Body.Value{ .InternalBlob = .{ .bytes = scheduled_response_buffer.toManaged(bun.default_allocator), }, }; this.scheduled_response_buffer = .{ .allocator = this.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }; return response; } fn toResponse(this: *FetchTasklet, allocator: std.mem.Allocator) Response { log("toResponse", .{}); std.debug.assert(this.metadata != null); // at this point we always should have metadata var metadata = this.metadata.?; const http_response = metadata.response; this.is_waiting_body = this.result.has_more; return Response{ .allocator = allocator, .url = bun.String.createAtomIfPossible(metadata.url), .status_text = bun.String.createAtomIfPossible(http_response.status), .redirected = this.result.redirected, .body = .{ .init = .{ .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), .status_code = @as(u16, @truncate(http_response.status_code)), }, .value = this.toBodyValue(), }, }; } pub fn onResolve(this: *FetchTasklet) JSValue { log("onResolve", .{}); const allocator = bun.default_allocator; var response = allocator.create(Response) catch unreachable; response.* = this.toResponse(allocator); const response_js = Response.makeMaybePooled(@as(js.JSContextRef, this.global_this), response); response_js.ensureStillAlive(); this.response = JSC.Strong.create(response_js, this.global_this); return response_js; } pub fn get( allocator: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, promise: JSC.JSPromise.Strong, fetch_options: FetchOptions, ) !*FetchTasklet { var jsc_vm = globalThis.bunVM(); var fetch_tasklet = try allocator.create(FetchTasklet); fetch_tasklet.* = .{ .mutex = Mutex.init(), .scheduled_response_buffer = .{ .allocator = fetch_options.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }, .response_buffer = MutableString{ .allocator = fetch_options.memory_reporter.allocator(), .list = .{ .items = &.{}, .capacity = 0, }, }, .http = try allocator.create(HTTPClient.AsyncHTTP), .javascript_vm = jsc_vm, .request_body = fetch_options.body, .global_this = globalThis, .request_headers = fetch_options.headers, .promise = promise, .url_proxy_buffer = fetch_options.url_proxy_buffer, .signal = fetch_options.signal, .hostname = fetch_options.hostname, .tracker = JSC.AsyncTaskTracker.init(jsc_vm), .memory_reporter = fetch_options.memory_reporter, .check_server_identity = fetch_options.check_server_identity, .reject_unauthorized = fetch_options.reject_unauthorized, }; fetch_tasklet.signals = fetch_tasklet.signal_store.to(); fetch_tasklet.tracker.didSchedule(globalThis); 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); } if (fetch_tasklet.check_server_identity.has() and fetch_tasklet.reject_unauthorized) { fetch_tasklet.signal_store.cert_errors.store(true, .Monotonic); } else { fetch_tasklet.signals.cert_errors = null; // we use aborted to signal that we should abort reject_unauthorized after check with check_server_identity if (fetch_tasklet.signal == null) { fetch_tasklet.signals.aborted = null; } } fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init( fetch_options.memory_reporter.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, fetch_options.hostname, fetch_options.redirect_type, fetch_tasklet.signals, ); if (fetch_options.redirect_type != FetchRedirect.follow) { 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; fetch_tasklet.http.?.client.disable_decompression = fetch_options.disable_decompression; fetch_tasklet.http.?.client.reject_unauthorized = fetch_options.reject_unauthorized; // we wanna to return after headers are received fetch_tasklet.signal_store.header_progress.store(true, .Monotonic); if (fetch_tasklet.request_body == .Sendfile) { std.debug.assert(fetch_options.url.isHTTP()); std.debug.assert(fetch_options.proxy == null); fetch_tasklet.http.?.request_body = .{ .sendfile = fetch_tasklet.request_body.Sendfile }; } 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.signal_store.aborted.store(true, .Monotonic); this.tracker.didCancel(this.global_this); if (this.http != null) { HTTPClient.http_thread.scheduleShutdown(this.http.?); } } const FetchOptions = struct { method: Method, headers: Headers, body: HTTPRequestBody, timeout: usize, disable_timeout: bool, disable_keepalive: bool, disable_decompression: bool, reject_unauthorized: bool, url: ZigURL, verbose: bool = false, redirect_type: FetchRedirect = FetchRedirect.follow, proxy: ?ZigURL = null, url_proxy_buffer: []const u8 = "", signal: ?*JSC.WebCore.AbortSignal = null, globalThis: ?*JSGlobalObject, // Custom Hostname hostname: ?[]u8 = null, memory_reporter: *JSC.MemoryReportingAllocator, check_server_identity: JSC.Strong = .{}, }; pub fn queue( allocator: std.mem.Allocator, global: *JSGlobalObject, fetch_options: FetchOptions, promise: JSC.JSPromise.Strong, ) !*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.mutex.lock(); defer task.mutex.unlock(); log("callback success {} has_more {} bytes {}", .{ result.isSuccess(), result.has_more, result.body.?.list.items.len }); task.result = result; // metadata should be provided only once so we preserve it until we consume it if (result.metadata) |metadata| { log("added callback metadata", .{}); std.debug.assert(task.metadata == null); task.metadata = metadata; } task.body_size = result.body_size; const success = result.isSuccess(); task.response_buffer = result.body.?.*; if (success) { _ = task.scheduled_response_buffer.write(task.response_buffer.list.items) catch @panic("OOM"); } // reset for reuse task.response_buffer.reset(); if (task.has_schedule_callback.compareAndSwap(false, true, .Acquire, .Monotonic)) |has_schedule_callback| { if (has_schedule_callback) { return; } } task.javascript_vm.eventLoop().enqueueTaskConcurrent(task.concurrent_task.from(task, .manual_deinit)); } }; fn dataURLResponse( _data_url: DataURL, globalThis: *JSGlobalObject, allocator: std.mem.Allocator, ) JSValue { var data_url = _data_url; const data = data_url.decodeData(allocator) catch { const err = JSC.createError(globalThis, "failed to fetch the data URL", .{}); return JSPromise.rejectedPromiseValue(globalThis, err); }; var blob = Blob.init(data, allocator, globalThis); var allocated = false; const mime_type = bun.HTTP.MimeType.init(data_url.mime_type, allocator, &allocated); blob.content_type = mime_type.value; if (allocated) { blob.content_type_allocated = true; } var response = allocator.create(Response) catch @panic("out of memory"); response.* = Response{ .body = Body{ .init = Body.Init{ .status_code = 200, }, .value = .{ .Blob = blob, }, }, .allocator = allocator, .status_text = bun.String.createAtom("OK"), .url = data_url.url.dupeRef(), }; return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); } pub export fn Bun__fetch( ctx: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); const globalThis = ctx.ptr(); const arguments = callframe.arguments(2); var exception_val = [_]JSC.C.JSValueRef{null}; var exception: JSC.C.ExceptionRef = &exception_val; var memory_reporter = bun.default_allocator.create(JSC.MemoryReportingAllocator) catch @panic("out of memory"); var free_memory_reporter = false; var allocator = memory_reporter.wrap(bun.default_allocator); defer { if (exception.* != null) { free_memory_reporter = true; ctx.throwValue(JSC.JSValue.c(exception.*)); } memory_reporter.report(globalThis.vm()); if (free_memory_reporter) bun.default_allocator.destroy(memory_reporter); } if (arguments.len == 0) { const err = JSC.toTypeError(.ERR_MISSING_ARGS, fetch_error_no_args, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err); } var headers: ?Headers = null; var method = Method.GET; var script_ctx = globalThis.bunVM(); var args = JSC.Node.ArgumentsSlice.init(script_ctx, arguments.ptr[0..arguments.len]); var url = ZigURL{}; var first_arg = args.nextEat().?; // We must always get the Body before the Headers That way, we can set // the Content-Type header from the Blob if no Content-Type header is // set in the Headers // // which is important for FormData. // https://github.com/oven-sh/bun/issues/2264 // var body: AnyBlob = AnyBlob{ .Blob = .{}, }; var disable_timeout = false; var disable_keepalive = false; var disable_decompression = false; var verbose = script_ctx.log.level.atLeast(.debug); var proxy: ?ZigURL = null; var redirect_type: FetchRedirect = FetchRedirect.follow; var signal: ?*JSC.WebCore.AbortSignal = null; // Custom Hostname var hostname: ?[]u8 = null; var url_proxy_buffer: []const u8 = undefined; var is_file_url = false; var reject_unauthorized = script_ctx.bundler.env.getTLSRejectUnauthorized(); var check_server_identity: JSValue = .zero; // TODO: move this into a DRYer implementation // The status quo is very repetitive and very bug prone if (first_arg.as(Request)) |request| { request.ensureURL() catch unreachable; if (request.url.isEmpty()) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } if (request.url.hasPrefixComptime("data:")) { var url_slice = request.url.toUTF8WithoutRef(allocator); defer url_slice.deinit(); var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch { const err = JSC.createError(globalThis, "failed to fetch the data URL", .{}); return JSPromise.rejectedPromiseValue(globalThis, err); }; data_url.url = request.url; return dataURLResponse(data_url, globalThis, allocator); } url = ZigURL.fromString(allocator, request.url) catch { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } free_memory_reporter = true; return JSPromise.rejectedPromiseValue( globalThis, err, ); }; is_file_url = url.isFile(); url_proxy_buffer = url.href; if (!is_file_url) { if (args.nextEat()) |options| { if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { var slice_ = method_.toSlice(ctx.ptr(), allocator); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } else { method = request.method; } 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 { // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } // an error was thrown return JSC.JSValue.jsUndefined(); } } else { body = request.body.value.useAsAnyBlob(); } if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; headers__.deref(); } else if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } } else if (request.headers) |head| { headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } 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.getOptionalEnum(ctx, "redirect", FetchRedirect) catch { return .zero; }) |redirect_value| { redirect_type = redirect_value; } 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(ctx, "decompress")) |decompress| { if (decompress.isBoolean()) { disable_decompression = !decompress.asBoolean(); } else if (decompress.isNumber()) { disable_decompression = decompress.to(i32) == 0; } } if (options.get(ctx, "tls")) |tls| { if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) { if (tls.get(ctx, "rejectUnauthorized")) |reject| { if (reject.isBoolean()) { reject_unauthorized = reject.asBoolean(); } else if (reject.isNumber()) { reject_unauthorized = reject.to(i32) != 0; } } if (tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| { if (checkServerIdentity.isCell() and checkServerIdentity.isCallable(globalThis.vm())) { check_server_identity = checkServerIdentity; } } } } if (options.get(globalThis, "proxy")) |proxy_arg| { if (proxy_arg.isString() and proxy_arg.getLength(ctx) > 0) { var href = JSC.URL.hrefFromJS(proxy_arg, globalThis); if (href.tag == .Dead) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } allocator.free(url_proxy_buffer); return JSPromise.rejectedPromiseValue(globalThis, err); } defer href.deref(); var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { globalThis.throwOutOfMemory(); return .zero; }; url = ZigURL.parse(buffer[0..url.href.len]); is_file_url = url.isFile(); proxy = ZigURL.parse(buffer[url.href.len..]); allocator.free(url_proxy_buffer); url_proxy_buffer = buffer; } } } } else { method = request.method; body = request.body.value.useAsAnyBlob(); if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable; } if (request.signal) |signal_| { _ = signal_.ref(); signal = signal_; } } } } else if (bun.String.tryFromJS(first_arg, globalThis)) |str| { if (str.isEmpty()) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } return JSPromise.rejectedPromiseValue(globalThis, err); } if (str.hasPrefixComptime("data:")) { var url_slice = str.toUTF8WithoutRef(allocator); defer url_slice.deinit(); var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch { const err = JSC.createError(globalThis, "failed to fetch the data URL", .{}); return JSPromise.rejectedPromiseValue(globalThis, err); }; data_url.url = str; return dataURLResponse(data_url, globalThis, allocator); } url = ZigURL.fromString(allocator, str) catch { // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err); }; url_proxy_buffer = url.href; is_file_url = url.isFile(); if (!is_file_url) { if (args.nextEat()) |options| { if (options.isObject() or options.jsType() == .DOMWrapper) { if (options.fastGet(ctx.ptr(), .method)) |method_| { var slice_ = method_.toSlice(ctx.ptr(), allocator); defer slice_.deinit(); method = Method.which(slice_.slice()) orelse .GET; } 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 { // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } // an error was thrown return JSC.JSValue.jsUndefined(); } } if (options.fastGet(ctx.ptr(), .headers)) |headers_| { if (headers_.as(FetchHeaders)) |headers__| { if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; // TODO: make this one pass } else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| { defer headers__.deref(); if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { if (hostname) |host| { allocator.free(host); } hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable; } headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable; } else { // Converting the headers failed; return null and // let the set exception get thrown return .zero; } } 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.getOptionalEnum(ctx, "redirect", FetchRedirect) catch { return .zero; }) |redirect_value| { redirect_type = redirect_value; } 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(ctx, "decompress")) |decompress| { if (decompress.isBoolean()) { disable_decompression = !decompress.asBoolean(); } else if (decompress.isNumber()) { disable_decompression = decompress.to(i32) == 0; } } if (options.get(ctx, "tls")) |tls| { if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) { if (tls.get(ctx, "rejectUnauthorized")) |reject| { if (reject.isBoolean()) { reject_unauthorized = reject.asBoolean(); } else if (reject.isNumber()) { reject_unauthorized = reject.to(i32) != 0; } } if (tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| { if (checkServerIdentity.isCell() and checkServerIdentity.isCallable(globalThis.vm())) { check_server_identity = checkServerIdentity; } } } } if (options.getTruthy(globalThis, "proxy")) |proxy_arg| { if (proxy_arg.isString() and proxy_arg.getLength(globalThis) > 0) { var href = JSC.URL.hrefFromJS(proxy_arg, globalThis); if (href.tag == .Dead) { const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); // clean hostname if any if (hostname) |host| { allocator.free(host); hostname = null; } allocator.free(url_proxy_buffer); free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } defer href.deref(); var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch { globalThis.throwOutOfMemory(); return .zero; }; url = ZigURL.parse(buffer[0..url.href.len]); proxy = ZigURL.parse(buffer[url.href.len..]); allocator.free(url_proxy_buffer); url_proxy_buffer = buffer; } } } } } } else { const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, first_arg.asRef())); const err = JSC.toTypeError(.ERR_INVALID_ARG_TYPE, "{s}", .{fetch_error}, ctx); exception.* = err.asObjectRef(); return .zero; } if (url.isEmpty()) { free_memory_reporter = true; const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx); return JSPromise.rejectedPromiseValue(globalThis, err); } // This is not 100% correct. // We don't pass along headers, we ignore method, we ignore status code... // But it's better than status quo. if (is_file_url) { defer allocator.free(url_proxy_buffer); var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const PercentEncoding = @import("../../url.zig").PercentEncoding; var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined; var stream = std.io.fixedBufferStream(&path_buf2); const url_path_decoded = path_buf2[0 .. PercentEncoding.decode( @TypeOf(&stream.writer()), &stream.writer(), url.path, ) catch { globalThis.throwOutOfMemory(); return .zero; }]; const temp_file_path = bun.path.joinAbsStringBuf( globalThis.bunVM().bundler.fs.top_level_dir, &path_buf, &[_]string{ globalThis.bunVM().main, "../", url_path_decoded, }, .auto, ); var file_url_string = JSC.URL.fileURLFromString(bun.String.fromUTF8(temp_file_path)); defer file_url_string.deref(); const bun_file = Blob.findOrCreateFileFromPath( .{ .path = .{ .string = bun.PathString.init( temp_file_path, ), }, }, globalThis, ); var response = bun.default_allocator.create(Response) catch @panic("out of memory"); response.* = Response{ .body = Body{ .init = Body.Init{ .status_code = 200, }, .value = .{ .Blob = bun_file }, }, .allocator = bun.default_allocator, .url = file_url_string.clone(), }; return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); } if (url.protocol.len > 0) { if (!(url.isHTTP() or url.isHTTPS())) { defer allocator.free(url_proxy_buffer); const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http: or https:", .{}, ctx); free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } } if (!method.hasRequestBody() and body.size() > 0) { defer allocator.free(url_proxy_buffer); const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); free_memory_reporter = true; return JSPromise.rejectedPromiseValue(globalThis, err); } if (headers == null and body.size() > 0 and body.hasContentTypeFromUser()) { headers = Headers.from( null, allocator, .{ .body = &body }, ) catch unreachable; } var http_body = FetchTasklet.HTTPRequestBody{ .AnyBlob = body, }; if (body.needsToReadFile()) { prepare_body: { const opened_fd_res: JSC.Node.Maybe(bun.FileDescriptor) = switch (body.Blob.store.?.data.file.pathlike) { .fd => |fd| bun.sys.dup(fd), .path => |path| bun.sys.open(path.sliceZ(&globalThis.bunVM().nodeFS().sync_error_buf), std.os.O.RDONLY | std.os.O.NOCTTY, 0), }; const opened_fd = switch (opened_fd_res) { .err => |err| { allocator.free(url_proxy_buffer); const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); body.detach(); if (headers) |*headers_| { headers_.buf.deinit(allocator); headers_.entries.deinit(allocator); } free_memory_reporter = true; return rejected_value; }, .result => |fd| fd, }; if (proxy == null and bun.HTTP.Sendfile.isEligible(url)) { use_sendfile: { const stat: bun.Stat = switch (bun.sys.fstat(opened_fd)) { .result => |result| result, // bail out for any reason .err => break :use_sendfile, }; if (Environment.isMac) { // macOS only supports regular files for sendfile() if (!bun.isRegularFile(stat.mode)) { break :use_sendfile; } } // if it's < 32 KB, it's not worth it if (stat.size < 32 * 1024) { break :use_sendfile; } const original_size = body.Blob.size; const stat_size = @as(Blob.SizeType, @intCast(stat.size)); const blob_size = if (bun.isRegularFile(stat.mode)) stat_size else @min(original_size, stat_size); http_body = .{ .Sendfile = .{ .fd = opened_fd, .remain = body.Blob.offset + original_size, .offset = body.Blob.offset, .content_size = blob_size, }, }; if (bun.isRegularFile(stat.mode)) { http_body.Sendfile.offset = @min(http_body.Sendfile.offset, stat_size); http_body.Sendfile.remain = @min(@max(http_body.Sendfile.remain, http_body.Sendfile.offset), stat_size) -| http_body.Sendfile.offset; } body.detach(); break :prepare_body; } } // TODO: make this async + lazy const res = JSC.Node.NodeFS.readFile( globalThis.bunVM().nodeFS(), .{ .encoding = .buffer, .path = .{ .fd = opened_fd }, .offset = body.Blob.offset, .max_size = body.Blob.size, }, .sync, ); if (body.Blob.store.?.data.file.pathlike == .path) { _ = bun.sys.close(opened_fd); } switch (res) { .err => |err| { allocator.free(url_proxy_buffer); free_memory_reporter = true; const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); body.detach(); if (headers) |*headers_| { headers_.buf.deinit(allocator); headers_.entries.deinit(allocator); } return rejected_value; }, .result => |result| { body.detach(); body.from(std.ArrayList(u8).fromOwnedSlice(allocator, @constCast(result.slice()))); http_body = .{ .AnyBlob = body }; }, } } } // Only create this after we have validated all the input. // or else we will leak it var promise = JSPromise.Strong.init(globalThis); const promise_val = promise.value(); // var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType) _ = FetchTasklet.queue( allocator, globalThis, .{ .method = method, .url = url, .headers = headers orelse Headers{ .allocator = allocator, }, .body = http_body, .timeout = std.time.ns_per_hour, .disable_keepalive = disable_keepalive, .disable_timeout = disable_timeout, .disable_decompression = disable_decompression, .reject_unauthorized = reject_unauthorized, .redirect_type = redirect_type, .verbose = verbose, .proxy = proxy, .url_proxy_buffer = url_proxy_buffer, .signal = signal, .globalThis = globalThis, .hostname = hostname, .memory_reporter = memory_reporter, .check_server_identity = if (check_server_identity.isEmptyOrUndefinedOrNull()) .{} else JSC.Strong.create(check_server_identity, globalThis), }, // Pass the Strong value instead of creating a new one, or else we // will leak it // see https://github.com/oven-sh/bun/issues/2985 promise, ) catch unreachable; return promise_val; } }; // 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 const Options = struct { body: ?*const AnyBlob = null, }; pub fn from(fetch_headers_ref: ?*FetchHeaders, allocator: std.mem.Allocator, options: Options) !Headers { var header_count: u32 = 0; var buf_len: u32 = 0; if (fetch_headers_ref) |headers_ref| headers_ref.count(&header_count, &buf_len); var headers = Headers{ .entries = .{}, .buf = .{}, .allocator = allocator, }; const buf_len_before_content_type = buf_len; const needs_content_type = brk: { if (options.body) |body| { if (body.hasContentTypeFromUser() and (fetch_headers_ref == null or !fetch_headers_ref.?.fastHas(.ContentType))) { header_count += 1; buf_len += @as(u32, @truncate(body.contentType().len + "Content-Type".len)); break :brk true; } } break :brk false; }; 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); if (fetch_headers_ref) |headers_ref| headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr); // TODO: maybe we should send Content-Type header first instead of last? if (needs_content_type) { bun.copy(u8, headers.buf.items[buf_len_before_content_type..], "Content-Type"); names[header_count - 1] = .{ .offset = buf_len_before_content_type, .length = "Content-Type".len, }; bun.copy(u8, headers.buf.items[buf_len_before_content_type + "Content-Type".len ..], options.body.?.contentType()); values[header_count - 1] = .{ .offset = buf_len_before_content_type + @as(u32, "Content-Type".len), .length = @as(u32, @truncate(options.body.?.contentType().len)), }; } return headers; } };