diff --git a/src/bun.js/webcore/fetch.zig b/src/bun.js/webcore/fetch.zig new file mode 100644 index 0000000000..3898b23d83 --- /dev/null +++ b/src/bun.js/webcore/fetch.zig @@ -0,0 +1,2767 @@ +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."; +pub const fetch_error_proxy_unix = "fetch() cannot use a proxy with a unix socket."; +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 FetchTasklet = struct { + pub const FetchTaskletStream = JSC.WebCore.NetworkSink; + + const log = Output.scoped(.FetchTasklet, false); + sink: ?*FetchTaskletStream.JSSink = null, + http: ?*http.AsyncHTTP = null, + result: http.HTTPClientResult = .{}, + metadata: ?http.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 weak ref we need this to track the response JS lifetime + response: JSC.Weak(FetchTasklet) = .{}, + /// native response ref if we still need it when JS is discarted + native_response: ?*Response = null, + ignore_data: bool = false, + /// 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: Async.KeepAlive = .{}, + 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: http.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: http.Signals = .{}, + signal_store: http.Signals.Store = .{}, + has_schedule_callback: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + + // must be stored because AbortSignal stores reason weakly + abort_reason: JSC.Strong = .empty, + + // custom checkServerIdentity + check_server_identity: JSC.Strong = .empty, + reject_unauthorized: bool = true, + // Custom Hostname + hostname: ?[]u8 = null, + is_waiting_body: bool = false, + is_waiting_abort: bool = false, + is_waiting_request_stream_start: bool = false, + mutex: Mutex, + + tracker: JSC.AsyncTaskTracker, + + ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), + + pub fn ref(this: *FetchTasklet) void { + const count = this.ref_count.fetchAdd(1, .monotonic); + bun.debugAssert(count > 0); + } + + pub fn deref(this: *FetchTasklet) void { + const count = this.ref_count.fetchSub(1, .monotonic); + bun.debugAssert(count > 0); + + if (count == 1) { + this.deinit(); + } + } + + pub fn derefFromThread(this: *FetchTasklet) void { + const count = this.ref_count.fetchSub(1, .monotonic); + bun.debugAssert(count > 0); + + if (count == 1) { + // this is really unlikely to happen, but can happen + // lets make sure that we always call deinit from main thread + + this.javascript_vm.eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(this, FetchTasklet.deinit)); + } + } + + pub const HTTPRequestBody = union(enum) { + AnyBlob: AnyBlob, + Sendfile: http.Sendfile, + ReadableStream: JSC.WebCore.ReadableStream.Strong, + + pub const Empty: HTTPRequestBody = .{ .AnyBlob = .{ .Blob = .{} } }; + + pub fn store(this: *HTTPRequestBody) ?*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(), + .ReadableStream => |*stream| { + stream.deinit(); + }, + .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 fromJS(globalThis: *JSGlobalObject, value: JSValue) bun.JSError!HTTPRequestBody { + var body_value = try Body.Value.fromJS(globalThis, value); + if (body_value == .Used or (body_value == .Locked and (body_value.Locked.action != .none or body_value.Locked.isDisturbed2(globalThis)))) { + return globalThis.ERR_BODY_ALREADY_USED("body already used", .{}).throw(); + } + if (body_value == .Locked) { + if (body_value.Locked.readable.has()) { + // just grab the ref + return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; + } + const readable = body_value.toReadableStream(globalThis); + if (!readable.isEmptyOrUndefinedOrNull() and body_value == .Locked and body_value.Locked.readable.has()) { + return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; + } + } + return FetchTasklet.HTTPRequestBody{ .AnyBlob = body_value.useAsAnyBlob() }; + } + + pub fn needsToReadFile(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.needsToReadFile(), + else => false, + }; + } + + pub fn isS3(this: *const HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |*blob| blob.isS3(), + else => false, + }; + } + + pub fn hasContentTypeFromUser(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.hasContentTypeFromUser(), + else => false, + }; + } + + pub fn getAnyBlob(this: *HTTPRequestBody) ?*AnyBlob { + return switch (this.*) { + .AnyBlob => &this.AnyBlob, + else => null, + }; + } + + pub fn hasBody(this: *HTTPRequestBody) bool { + return switch (this.*) { + .AnyBlob => |blob| blob.size() > 0, + .ReadableStream => |*stream| stream.has(), + .Sendfile => true, + }; + } + }; + + pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { + return FetchTasklet{}; + } + + fn clearSink(this: *FetchTasklet) void { + if (this.sink) |wrapper| { + this.sink = null; + + wrapper.sink.done = true; + wrapper.sink.ended = true; + wrapper.sink.finalize(); + wrapper.detach(); + wrapper.sink.finalizeAndDestroy(); + } + } + + 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; + } + + if (this.result.certificate_info) |*certificate| { + certificate.deinit(bun.default_allocator); + this.result.certificate_info = null; + } + + this.request_headers.entries.deinit(allocator); + this.request_headers.buf.deinit(allocator); + this.request_headers = Headers{ .allocator = undefined }; + + if (this.http) |http_| { + http_.clearData(); + } + + if (this.metadata != null) { + this.metadata.?.deinit(allocator); + this.metadata = null; + } + + this.response_buffer.deinit(); + this.response.deinit(); + if (this.native_response) |response| { + this.native_response = null; + + response.unref(); + } + + this.readable_stream_ref.deinit(); + + this.scheduled_response_buffer.deinit(); + if (this.request_body != .ReadableStream or this.is_waiting_request_stream_start) { + this.request_body.detach(); + } + + this.abort_reason.deinit(); + this.check_server_identity.deinit(); + this.clearAbortSignal(); + } + + pub fn deinit(this: *FetchTasklet) void { + log("deinit", .{}); + + bun.assert(this.ref_count.load(.monotonic) == 0); + + this.clearData(); + + var reporter = this.memory_reporter; + const allocator = reporter.allocator(); + + if (this.http) |http_| { + this.http = null; + allocator.destroy(http_); + } + allocator.destroy(this); + // reporter.assert(); + bun.default_allocator.destroy(reporter); + } + + fn getCurrentResponse(this: *FetchTasklet) ?*Response { + // we need a body to resolve the promise when buffering + if (this.native_response) |response| { + return response; + } + + // if we did not have a direct reference we check if the Weak ref is still alive + if (this.response.get()) |response_js| { + if (response_js.as(Response)) |response| { + return response; + } + } + + return null; + } + + pub fn onResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var args = callframe.arguments_old(2); + var this: *@This() = args.ptr[args.len - 1].asPromisePtr(@This()); + defer this.deref(); + if (this.request_body == .ReadableStream) { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer readable_stream_ref.deinit(); + if (readable_stream_ref.get(globalThis)) |stream| { + stream.done(globalThis); + this.clearSink(); + } + } + + return JSValue.jsUndefined(); + } + + pub fn onRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2); + var this = args.ptr[args.len - 1].asPromisePtr(@This()); + defer this.deref(); + const err = args.ptr[0]; + if (this.request_body == .ReadableStream) { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer readable_stream_ref.deinit(); + if (readable_stream_ref.get(globalThis)) |stream| { + stream.cancel(globalThis); + this.clearSink(); + } + } + + this.abortListener(err); + return JSValue.jsUndefined(); + } + comptime { + const jsonResolveRequestStream = JSC.toJSHostFunction(onResolveRequestStream); + @export(&jsonResolveRequestStream, .{ .name = "Bun__FetchTasklet__onResolveRequestStream" }); + const jsonRejectRequestStream = JSC.toJSHostFunction(onRejectRequestStream); + @export(&jsonRejectRequestStream, .{ .name = "Bun__FetchTasklet__onRejectRequestStream" }); + } + + pub fn startRequestStream(this: *FetchTasklet) void { + this.is_waiting_request_stream_start = false; + bun.assert(this.request_body == .ReadableStream); + if (this.request_body.ReadableStream.get(this.global_this)) |stream| { + this.ref(); // lets only unref when sink is done + + const globalThis = this.global_this; + var response_stream = FetchTaskletStream.new(.{ + .task = .{ .fetch = this }, + .buffer = .{}, + .globalThis = globalThis, + }).toSink(); + var signal = &response_stream.sink.signal; + this.sink = response_stream; + + signal.* = FetchTaskletStream.JSSink.SinkSignal.init(JSValue.zero); + + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + signal.clear(); + bun.assert(signal.isDead()); + + // We are already corked! + const assignment_result: JSValue = FetchTaskletStream.JSSink.assignToStream( + globalThis, + stream.value, + response_stream, + @as(**anyopaque, @ptrCast(&signal.ptr)), + ); + + assignment_result.ensureStillAlive(); + + // assert that it was updated + bun.assert(!signal.isDead()); + + if (assignment_result.toError()) |err_value| { + response_stream.detach(); + this.sink = null; + response_stream.sink.finalizeAndDestroy(); + return this.abortListener(err_value); + } + + if (!assignment_result.isEmptyOrUndefinedOrNull()) { + assignment_result.ensureStillAlive(); + // it returns a Promise when it goes through ReadableStreamDefaultReader + if (assignment_result.asAnyPromise()) |promise| { + switch (promise.status(globalThis.vm())) { + .pending => { + this.ref(); + assignment_result.then( + globalThis, + this, + onResolveRequestStream, + onRejectRequestStream, + ); + }, + .fulfilled => { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer { + stream.done(globalThis); + this.clearSink(); + readable_stream_ref.deinit(); + } + }, + .rejected => { + var readable_stream_ref = this.request_body.ReadableStream; + this.request_body.ReadableStream = .{}; + defer { + stream.cancel(globalThis); + this.clearSink(); + readable_stream_ref.deinit(); + } + + this.abortListener(promise.result(globalThis.vm())); + }, + } + return; + } else { + // if is not a promise we treat it as Error + response_stream.detach(); + this.sink = null; + response_stream.sink.finalizeAndDestroy(); + return this.abortListener(assignment_result); + } + } + } + } + pub fn onBodyReceived(this: *FetchTasklet) void { + const success = this.result.isSuccess(); + const globalThis = this.global_this; + // reset the buffer if we are streaming or if we are not waiting for bufferig anymore + var buffer_reset = true; + defer { + if (buffer_reset) { + this.scheduled_response_buffer.reset(); + } + } + + if (!success) { + var err = this.onReject(); + var need_deinit = true; + defer if (need_deinit) err.deinit(); + // if we are streaming update with error + if (this.readable_stream_ref.get(globalThis)) |readable| { + if (readable.ptr == .Bytes) { + readable.ptr.Bytes.onData( + .{ + .err = .{ .JSValue = err.toJS(globalThis) }, + }, + bun.default_allocator, + ); + } + } + // if we are buffering resolve the promise + if (this.getCurrentResponse()) |response| { + response.body.value.toErrorInstance(err, globalThis); + need_deinit = false; // body value now owns the error + const body = response.body; + if (body.value == .Locked) { + if (body.value.Locked.promise) |promise_| { + const promise = promise_.asAnyPromise().?; + promise.reject(globalThis, response.body.value.Error.toJS(globalThis)); + } + } + } + return; + } + + if (this.readable_stream_ref.get(globalThis)) |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 + const 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, + ); + } else { + var prev = this.readable_stream_ref; + this.readable_stream_ref = .{}; + defer prev.deinit(); + buffer_reset = false; + this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); + this.scheduled_response_buffer = .{ + .allocator = bun.default_allocator, + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }; + readable.ptr.Bytes.onData( + .{ + .owned_and_done = bun.ByteList.initConst(chunk), + }, + bun.default_allocator, + ); + } + return; + } + } + + if (this.getCurrentResponse()) |response| { + var body = &response.body; + if (body.value == .Locked) { + if (body.value.Locked.readable.get(globalThis)) |readable| { + if (readable.ptr == .Bytes) { + readable.ptr.Bytes.size_hint = this.getSizeHint(); + + const 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, + ); + } else { + var prev = body.value.Locked.readable; + body.value.Locked.readable = .{}; + readable.value.ensureStillAlive(); + prev.deinit(); + readable.value.ensureStillAlive(); + 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, this is also the only case we dont wanna to reset the buffer + buffer_reset = false; + 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; + const 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, response.getFetchHeaders()); + } + } + } + } + } + + pub fn onProgressUpdate(this: *FetchTasklet) void { + JSC.markBinding(@src()); + log("onProgressUpdate", .{}); + this.mutex.lock(); + this.has_schedule_callback.store(false, .monotonic); + const is_done = !this.result.has_more; + + const vm = this.javascript_vm; + // vm is shutting down we cannot touch JS + if (vm.isShuttingDown()) { + this.mutex.unlock(); + if (is_done) { + this.deref(); + } + return; + } + + const globalThis = this.global_this; + defer { + this.mutex.unlock(); + // if we are not done we wait until the next call + if (is_done) { + var poll_ref = this.poll_ref; + this.poll_ref = .{}; + poll_ref.unref(vm); + this.deref(); + } + } + if (this.is_waiting_request_stream_start and this.result.can_stream) { + // start streaming + this.startRequestStream(); + } + // if we already respond the metadata and still need to process the body + if (this.is_waiting_body) { + this.onBodyReceived(); + return; + } + if (this.metadata == null and this.result.isSuccess()) return; + + // if we abort because of cert error + // we wait the Http Client because we already have the response + // we just need to deinit + if (this.is_waiting_abort) { + return; + } + const promise_value = this.promise.valueOrEmpty(); + + if (promise_value.isEmptyOrUndefinedOrNull()) { + log("onProgressUpdate: promise_value is null", .{}); + this.promise.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; + var result = this.onReject(); + defer result.deinit(); + + promise_value.ensureStillAlive(); + promise.reject(globalThis, result.toJS(globalThis)); + + tracker.didDispatch(globalThis); + this.promise.deinit(); + return; + } + // everything ok + if (this.metadata == null) { + log("onProgressUpdate: metadata is null", .{}); + return; + } + } + + const tracker = this.tracker; + tracker.willDispatch(globalThis); + defer { + log("onProgressUpdate: promise_value is not null", .{}); + tracker.didDispatch(globalThis); + this.promise.deinit(); + } + const success = this.result.isSuccess(); + const result = switch (success) { + true => JSC.Strong.create(this.onResolve(), globalThis), + false => brk: { + // in this case we wanna a JSC.Strong so we just convert it + var value = this.onReject(); + _ = value.toJS(globalThis); + break :brk value.JSValue; + }, + }; + + promise_value.ensureStillAlive(); + const Holder = struct { + held: JSC.Strong, + promise: JSC.Strong, + globalObject: *JSC.JSGlobalObject, + task: JSC.AnyTask, + + pub fn resolve(self: *@This()) void { + // cleanup + defer bun.default_allocator.destroy(self); + defer self.held.deinit(); + defer self.promise.deinit(); + // resolve the promise + var prom = self.promise.swap().asAnyPromise().?; + const res = self.held.swap(); + res.ensureStillAlive(); + prom.resolve(self.globalObject, res); + } + + pub fn reject(self: *@This()) void { + // cleanup + defer bun.default_allocator.destroy(self); + defer self.held.deinit(); + defer self.promise.deinit(); + + // reject the promise + var prom = self.promise.swap().asAnyPromise().?; + const res = self.held.swap(); + res.ensureStillAlive(); + prom.reject(self.globalObject, res); + } + }; + var holder = bun.default_allocator.create(Holder) catch bun.outOfMemory(); + holder.* = .{ + .held = result, + // we need the promise to be alive until the task is done + .promise = this.promise.strong, + .globalObject = globalThis, + .task = undefined, + }; + this.promise.strong = .empty; + holder.task = switch (success) { + true => JSC.AnyTask.New(Holder, Holder.resolve).init(holder), + false => JSC.AnyTask.New(Holder, Holder.reject).init(holder), + }; + + vm.enqueueTask(JSC.Task.init(&holder.task)); + } + + pub fn checkServerIdentity(this: *FetchTasklet, certificate_info: http.CertificateInfo) bool { + if (this.check_server_identity.get()) |check_server_identity| { + check_server_identity.ensureStillAlive(); + if (certificate_info.cert.len > 0) { + const cert = certificate_info.cert; + var cert_ptr = cert.ptr; + if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| { + const globalObject = this.global_this; + defer x509.free(); + const js_cert = X509.toJS(x509, globalObject) catch |err| { + switch (err) { + error.JSError => {}, + error.OutOfMemory => globalObject.throwOutOfMemory() catch {}, + } + const check_result = globalObject.tryTakeException().?; + // mark to wait until deinit + this.is_waiting_abort = this.result.has_more; + this.abort_reason.set(globalObject, check_result); + this.signal_store.aborted.store(true, .monotonic); + this.tracker.didCancel(this.global_this); + // we need to abort the request + if (this.http) |http_| http.http_thread.scheduleShutdown(http_); + this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; + return false; + }; + var hostname: bun.String = bun.String.createUTF8(certificate_info.hostname); + defer hostname.deref(); + const js_hostname = hostname.toJS(globalObject); + js_hostname.ensureStillAlive(); + js_cert.ensureStillAlive(); + const check_result = check_server_identity.call(globalObject, .undefined, &.{ js_hostname, js_cert }) catch |err| globalObject.takeException(err); + + // > Returns object [...] on failure + if (check_result.isAnyError()) { + // mark to wait until deinit + this.is_waiting_abort = this.result.has_more; + this.abort_reason.set(globalObject, check_result); + this.signal_store.aborted.store(true, .monotonic); + this.tracker.didCancel(this.global_this); + + // we need to abort the request + if (this.http) |http_| { + http.http_thread.scheduleShutdown(http_); + } + this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; + return false; + } + + // > On success, returns + // We treat any non-error value as a success. + return true; + } + } + } + this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; + return false; + } + + fn getAbortError(this: *FetchTasklet) ?Body.Value.ValueError { + if (this.abort_reason.has()) { + defer this.clearAbortSignal(); + const out = this.abort_reason; + + this.abort_reason = .empty; + return Body.Value.ValueError{ .JSValue = out }; + } + + if (this.signal) |signal| { + if (signal.reasonIfAborted(this.global_this)) |reason| { + defer this.clearAbortSignal(); + return reason.toBodyValueError(this.global_this); + } + } + + return null; + } + + fn clearAbortSignal(this: *FetchTasklet) void { + const signal = this.signal orelse return; + this.signal = null; + defer { + signal.pendingActivityUnref(); + signal.unref(); + } + + signal.cleanNativeBindings(this); + } + + pub fn onReject(this: *FetchTasklet) Body.Value.ValueError { + bun.assert(this.result.fail != null); + log("onReject", .{}); + + if (this.getAbortError()) |err| { + return err; + } + + if (this.result.abortReason()) |reason| { + return .{ .AbortReason = reason }; + } + + // some times we don't have metadata so we also check http.url + const path = if (this.metadata) |metadata| + bun.String.createUTF8(metadata.url) + else if (this.http) |http_| + bun.String.createUTF8(http_.url.href) + else + bun.String.empty; + + const fetch_error = JSC.SystemError{ + .code = bun.String.static(switch (this.result.fail.?) { + error.ConnectionClosed => "ECONNRESET", + else => |e| @errorName(e), + }), + .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?"), + error.RedirectURLInvalid => bun.String.static("Redirect URL in Location header is invalid."), + + error.UNABLE_TO_GET_ISSUER_CERT => bun.String.static("unable to get issuer certificate"), + error.UNABLE_TO_GET_CRL => bun.String.static("unable to get certificate CRL"), + error.UNABLE_TO_DECRYPT_CERT_SIGNATURE => bun.String.static("unable to decrypt certificate's signature"), + error.UNABLE_TO_DECRYPT_CRL_SIGNATURE => bun.String.static("unable to decrypt CRL's signature"), + error.UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY => bun.String.static("unable to decode issuer public key"), + error.CERT_SIGNATURE_FAILURE => bun.String.static("certificate signature failure"), + error.CRL_SIGNATURE_FAILURE => bun.String.static("CRL signature failure"), + error.CERT_NOT_YET_VALID => bun.String.static("certificate is not yet valid"), + error.CRL_NOT_YET_VALID => bun.String.static("CRL is not yet valid"), + error.CERT_HAS_EXPIRED => bun.String.static("certificate has expired"), + error.CRL_HAS_EXPIRED => bun.String.static("CRL has expired"), + error.ERROR_IN_CERT_NOT_BEFORE_FIELD => bun.String.static("format error in certificate's notBefore field"), + error.ERROR_IN_CERT_NOT_AFTER_FIELD => bun.String.static("format error in certificate's notAfter field"), + error.ERROR_IN_CRL_LAST_UPDATE_FIELD => bun.String.static("format error in CRL's lastUpdate field"), + error.ERROR_IN_CRL_NEXT_UPDATE_FIELD => bun.String.static("format error in CRL's nextUpdate field"), + error.OUT_OF_MEM => bun.String.static("out of memory"), + error.DEPTH_ZERO_SELF_SIGNED_CERT => bun.String.static("self signed certificate"), + error.SELF_SIGNED_CERT_IN_CHAIN => bun.String.static("self signed certificate in certificate chain"), + error.UNABLE_TO_GET_ISSUER_CERT_LOCALLY => bun.String.static("unable to get local issuer certificate"), + error.UNABLE_TO_VERIFY_LEAF_SIGNATURE => bun.String.static("unable to verify the first certificate"), + error.CERT_CHAIN_TOO_LONG => bun.String.static("certificate chain too long"), + error.CERT_REVOKED => bun.String.static("certificate revoked"), + error.INVALID_CA => bun.String.static("invalid CA certificate"), + error.INVALID_NON_CA => bun.String.static("invalid non-CA certificate (has CA markings)"), + error.PATH_LENGTH_EXCEEDED => bun.String.static("path length constraint exceeded"), + error.PROXY_PATH_LENGTH_EXCEEDED => bun.String.static("proxy path length constraint exceeded"), + error.PROXY_CERTIFICATES_NOT_ALLOWED => bun.String.static("proxy certificates not allowed, please set the appropriate flag"), + error.INVALID_PURPOSE => bun.String.static("unsupported certificate purpose"), + error.CERT_UNTRUSTED => bun.String.static("certificate not trusted"), + error.CERT_REJECTED => bun.String.static("certificate rejected"), + error.APPLICATION_VERIFICATION => bun.String.static("application verification failure"), + error.SUBJECT_ISSUER_MISMATCH => bun.String.static("subject issuer mismatch"), + error.AKID_SKID_MISMATCH => bun.String.static("authority and subject key identifier mismatch"), + error.AKID_ISSUER_SERIAL_MISMATCH => bun.String.static("authority and issuer serial number mismatch"), + error.KEYUSAGE_NO_CERTSIGN => bun.String.static("key usage does not include certificate signing"), + error.UNABLE_TO_GET_CRL_ISSUER => bun.String.static("unable to get CRL issuer certificate"), + error.UNHANDLED_CRITICAL_EXTENSION => bun.String.static("unhandled critical extension"), + error.KEYUSAGE_NO_CRL_SIGN => bun.String.static("key usage does not include CRL signing"), + error.KEYUSAGE_NO_DIGITAL_SIGNATURE => bun.String.static("key usage does not include digital signature"), + error.UNHANDLED_CRITICAL_CRL_EXTENSION => bun.String.static("unhandled critical CRL extension"), + error.INVALID_EXTENSION => bun.String.static("invalid or inconsistent certificate extension"), + error.INVALID_POLICY_EXTENSION => bun.String.static("invalid or inconsistent certificate policy extension"), + error.NO_EXPLICIT_POLICY => bun.String.static("no explicit policy"), + error.DIFFERENT_CRL_SCOPE => bun.String.static("Different CRL scope"), + error.UNSUPPORTED_EXTENSION_FEATURE => bun.String.static("Unsupported extension feature"), + error.UNNESTED_RESOURCE => bun.String.static("RFC 3779 resource not subset of parent's resources"), + error.PERMITTED_VIOLATION => bun.String.static("permitted subtree violation"), + error.EXCLUDED_VIOLATION => bun.String.static("excluded subtree violation"), + error.SUBTREE_MINMAX => bun.String.static("name constraints minimum and maximum not supported"), + error.UNSUPPORTED_CONSTRAINT_TYPE => bun.String.static("unsupported name constraint type"), + error.UNSUPPORTED_CONSTRAINT_SYNTAX => bun.String.static("unsupported or invalid name constraint syntax"), + error.UNSUPPORTED_NAME_SYNTAX => bun.String.static("unsupported or invalid name syntax"), + error.CRL_PATH_VALIDATION_ERROR => bun.String.static("CRL path validation error"), + error.SUITE_B_INVALID_VERSION => bun.String.static("Suite B: certificate version invalid"), + error.SUITE_B_INVALID_ALGORITHM => bun.String.static("Suite B: invalid public key algorithm"), + error.SUITE_B_INVALID_CURVE => bun.String.static("Suite B: invalid ECC curve"), + error.SUITE_B_INVALID_SIGNATURE_ALGORITHM => bun.String.static("Suite B: invalid signature algorithm"), + error.SUITE_B_LOS_NOT_ALLOWED => bun.String.static("Suite B: curve not allowed for this LOS"), + error.SUITE_B_CANNOT_SIGN_P_384_WITH_P_256 => bun.String.static("Suite B: cannot sign P-384 with P-256"), + error.HOSTNAME_MISMATCH => bun.String.static("Hostname mismatch"), + error.EMAIL_MISMATCH => bun.String.static("Email address mismatch"), + error.IP_ADDRESS_MISMATCH => bun.String.static("IP address mismatch"), + error.INVALID_CALL => bun.String.static("Invalid certificate verification context"), + error.STORE_LOOKUP => bun.String.static("Issuer certificate lookup error"), + error.NAME_CONSTRAINTS_WITHOUT_SANS => bun.String.static("Issuer has name constraints but leaf has no SANs"), + error.UNKKNOW_CERTIFICATE_VERIFICATION_ERROR => bun.String.static("unknown certificate verification error"), + + else => |e| bun.String.createFormat("{s} fetching \"{}\". For more information, pass `verbose: true` in the second argument to fetch()", .{ + @errorName(e), + path, + }) catch bun.outOfMemory(), + }, + .path = path, + }; + + return .{ .SystemError = fetch_error }; + } + + pub fn onReadableStreamAvailable(ctx: *anyopaque, globalThis: *JSC.JSGlobalObject, readable: JSC.WebCore.ReadableStream) void { + const this = bun.cast(*FetchTasklet, ctx); + this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis); + } + + 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), + .unknown => 0, + }; + } + + fn toBodyValue(this: *FetchTasklet) Body.Value { + if (this.getAbortError()) |err| { + return .{ .Error = err }; + } + 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) Response { + log("toResponse", .{}); + bun.assert(this.metadata != null); + // at this point we always should have metadata + const metadata = this.metadata.?; + const http_response = metadata.response; + this.is_waiting_body = this.result.has_more; + return Response{ + .url = bun.String.createAtomIfPossible(metadata.url), + .redirected = this.result.redirected, + .init = .{ + .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), + .status_code = @as(u16, @truncate(http_response.status_code)), + .status_text = bun.String.createAtomIfPossible(http_response.status), + }, + .body = .{ + .value = this.toBodyValue(), + }, + }; + } + + fn ignoreRemainingResponseBody(this: *FetchTasklet) void { + log("ignoreRemainingResponseBody", .{}); + // enabling streaming will make the http thread to drain into the main thread (aka stop buffering) + // without a stream ref, response body or response instance alive it will just ignore the result + if (this.http) |http_| { + http_.enableBodyStreaming(); + } + // we should not keep the process alive if we are ignoring the body + const vm = this.javascript_vm; + this.poll_ref.unref(vm); + // clean any remaining refereces + this.readable_stream_ref.deinit(); + this.response.deinit(); + + if (this.native_response) |response| { + response.unref(); + this.native_response = null; + } + + this.ignore_data = true; + } + + export fn Bun__FetchResponse_finalize(this: *FetchTasklet) callconv(.C) void { + log("onResponseFinalize", .{}); + if (this.native_response) |response| { + const body = response.body; + // Three scenarios: + // + // 1. We are streaming, in which case we should not ignore the body. + // 2. We were buffering, in which case + // 2a. if we have no promise, we should ignore the body. + // 2b. if we have a promise, we should keep loading the body. + // 3. We never started buffering, in which case we should ignore the body. + // + // Note: We cannot call .get() on the ReadableStreamRef. This is called inside a finalizer. + if (body.value != .Locked or this.readable_stream_ref.held.has()) { + // Scenario 1 or 3. + return; + } + + if (body.value.Locked.promise) |promise| { + if (promise.isEmptyOrUndefinedOrNull()) { + // Scenario 2b. + this.ignoreRemainingResponseBody(); + } + } else { + // Scenario 3. + this.ignoreRemainingResponseBody(); + } + } + } + comptime { + _ = Bun__FetchResponse_finalize; + } + + pub fn onResolve(this: *FetchTasklet) JSValue { + log("onResolve", .{}); + const response = bun.new(Response, this.toResponse()); + const response_js = Response.makeMaybePooled(@as(js.JSContextRef, this.global_this), response); + response_js.ensureStillAlive(); + this.response = JSC.Weak(FetchTasklet).create(response_js, this.global_this, .FetchResponse, this); + this.native_response = response.ref(); + return response_js; + } + + pub fn get( + allocator: std.mem.Allocator, + globalThis: *JSC.JSGlobalObject, + fetch_options: FetchOptions, + promise: JSC.JSPromise.Strong, + ) !*FetchTasklet { + var jsc_vm = globalThis.bunVM(); + var fetch_tasklet = try allocator.create(FetchTasklet); + + fetch_tasklet.* = .{ + .mutex = .{}, + .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(http.AsyncHTTP), + .javascript_vm = jsc_vm, + .request_body = fetch_options.body, + .global_this = globalThis, + .promise = promise, + .request_headers = fetch_options.headers, + .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.transpiler.env.getHttpProxyFor(fetch_options.url); + } + } else { + proxy = jsc_vm.transpiler.env.getHttpProxyFor(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; + } + + // This task gets queued on the HTTP thread. + fetch_tasklet.http.?.* = http.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(), + http.HTTPClientResult.Callback.New( + *FetchTasklet, + // handles response events (on headers, on body, etc.) + FetchTasklet.callback, + ).init(fetch_tasklet), + fetch_options.redirect_type, + .{ + .http_proxy = proxy, + .hostname = fetch_options.hostname, + .signals = fetch_tasklet.signals, + .unix_socket_path = fetch_options.unix_socket_path, + .disable_timeout = fetch_options.disable_timeout, + .disable_keepalive = fetch_options.disable_keepalive, + .disable_decompression = fetch_options.disable_decompression, + .reject_unauthorized = fetch_options.reject_unauthorized, + .verbose = fetch_options.verbose, + .tls_props = fetch_options.ssl_config, + }, + ); + // enable streaming the write side + const isStream = fetch_tasklet.request_body == .ReadableStream; + fetch_tasklet.http.?.client.flags.is_streaming_request_body = isStream; + fetch_tasklet.is_waiting_request_stream_start = isStream; + if (isStream) { + fetch_tasklet.http.?.request_body = .{ + .stream = .{ + .buffer = .{}, + .ended = false, + }, + }; + } + // TODO is this necessary? the http client already sets the redirect type, + // so manually setting it here seems redundant + if (fetch_options.redirect_type != FetchRedirect.follow) { + fetch_tasklet.http.?.client.remaining_redirect_count = 0; + } + + // we want to return after headers are received + fetch_tasklet.signal_store.header_progress.store(true, .monotonic); + + if (fetch_tasklet.request_body == .Sendfile) { + bun.assert(fetch_options.url.isHTTP()); + bun.assert(fetch_options.proxy == null); + fetch_tasklet.http.?.request_body = .{ .sendfile = fetch_tasklet.request_body.Sendfile }; + } + + if (fetch_tasklet.signal) |signal| { + signal.pendingActivityRef(); + 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.set(this.global_this, reason); + this.abortTask(); + if (this.sink) |wrapper| { + wrapper.sink.abort(); + return; + } + } + + pub fn sendRequestData(this: *FetchTasklet, data: []const u8, ended: bool) void { + if (this.http) |http_| { + http.http_thread.scheduleRequestWrite(http_, data, ended); + } else if (data.len != 3) { + bun.default_allocator.free(data); + } + } + + pub fn abortTask(this: *FetchTasklet) void { + this.signal_store.aborted.store(true, .monotonic); + this.tracker.didCancel(this.global_this); + + if (this.http) |http_| { + http.http_thread.scheduleShutdown(http_); + } + } + + const FetchOptions = struct { + method: Method, + headers: Headers, + body: HTTPRequestBody, + disable_timeout: bool, + disable_keepalive: bool, + disable_decompression: bool, + reject_unauthorized: bool, + url: ZigURL, + verbose: http.HTTPVerboseLevel = .none, + 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 = .empty, + unix_socket_path: ZigString.Slice, + ssl_config: ?*SSLConfig = null, + }; + + pub fn queue( + allocator: std.mem.Allocator, + global: *JSGlobalObject, + fetch_options: FetchOptions, + promise: JSC.JSPromise.Strong, + ) !*FetchTasklet { + http.HTTPThread.init(&.{}); + var node = try get( + allocator, + global, + fetch_options, + promise, + ); + + var batch = bun.ThreadPool.Batch{}; + node.http.?.schedule(allocator, &batch); + node.poll_ref.ref(global.bunVM()); + + // increment ref so we can keep it alive until the http client is done + node.ref(); + http.http_thread.schedule(batch); + + return node; + } + + /// Called from HTTP thread. Handles HTTP events received from socket. + pub fn callback(task: *FetchTasklet, async_http: *http.AsyncHTTP, result: http.HTTPClientResult) void { + // at this point only this thread is accessing result to is no race condition + const is_done = !result.has_more; + // we are done with the http client so we can deref our side + // this is a atomic operation and will enqueue a task to deinit on the main thread + defer if (is_done) task.derefFromThread(); + + task.mutex.lock(); + // we need to unlock before task.deref(); + defer task.mutex.unlock(); + task.http.?.* = async_http.*; + task.http.?.response_buffer = async_http.response_buffer; + + log("callback success={} has_more={} bytes={}", .{ result.isSuccess(), result.has_more, result.body.?.list.items.len }); + + const prev_metadata = task.result.metadata; + const prev_cert_info = task.result.certificate_info; + task.result = result; + + // Preserve pending certificate info if it was preovided in the previous update. + if (task.result.certificate_info == null) { + if (prev_cert_info) |cert_info| { + task.result.certificate_info = cert_info; + } + } + + // metadata should be provided only once + if (result.metadata orelse prev_metadata) |metadata| { + log("added callback metadata", .{}); + if (task.metadata == null) { + task.metadata = metadata; + } + + task.result.metadata = null; + } + + task.body_size = result.body_size; + + const success = result.isSuccess(); + task.response_buffer = result.body.?.*; + + if (task.ignore_data) { + task.response_buffer.reset(); + + if (task.scheduled_response_buffer.list.capacity > 0) { + task.scheduled_response_buffer.deinit(); + task.scheduled_response_buffer = .{ + .allocator = task.memory_reporter.allocator(), + .list = .{ + .items = &.{}, + .capacity = 0, + }, + }; + } + if (success and result.has_more) { + // we are ignoring the body so we should not receive more data, so will only signal when result.has_more = true + return; + } + } else { + if (success) { + _ = task.scheduled_response_buffer.write(task.response_buffer.list.items) catch bun.outOfMemory(); + } + // reset for reuse + task.response_buffer.reset(); + } + + if (task.has_schedule_callback.cmpxchgStrong(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 = bun.new( + Response, + Response{ + .body = Body{ + .value = .{ + .Blob = blob, + }, + }, + .init = Response.Init{ + .status_code = 200, + .status_text = bun.String.createAtomASCII("OK"), + }, + .url = data_url.url.dupeRef(), + }, + ); + + return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); +} + +comptime { + const Bun__fetchPreconnect = JSC.toJSHostFunction(Bun__fetchPreconnect_); + @export(&Bun__fetchPreconnect, .{ .name = "Bun__fetchPreconnect" }); +} +pub fn Bun__fetchPreconnect_( + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(1).slice(); + + if (arguments.len < 1) { + return globalObject.throwNotEnoughArguments("fetch.preconnect", 1, arguments.len); + } + + var url_str = try JSC.URL.hrefFromJS(arguments[0], globalObject); + defer url_str.deref(); + + if (globalObject.hasException()) { + return .zero; + } + + if (url_str.tag == .Dead) { + return globalObject.ERR_INVALID_ARG_TYPE("Invalid URL", .{}).throw(); + } + + if (url_str.isEmpty()) { + return globalObject.ERR_INVALID_ARG_TYPE(fetch_error_blank_url, .{}).throw(); + } + + const url = ZigURL.parse(url_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory()); + if (!url.isHTTP() and !url.isHTTPS() and !url.isS3()) { + bun.default_allocator.free(url.href); + return globalObject.throwInvalidArguments("URL must be HTTP or HTTPS", .{}); + } + + if (url.hostname.len == 0) { + bun.default_allocator.free(url.href); + return globalObject.ERR_INVALID_ARG_TYPE(fetch_error_blank_url, .{}).throw(); + } + + if (!url.hasValidPort()) { + bun.default_allocator.free(url.href); + return globalObject.throwInvalidArguments("Invalid port", .{}); + } + + bun.http.AsyncHTTP.preconnect(url, true); + return .undefined; +} + +const StringOrURL = struct { + pub fn fromJS(value: JSC.JSValue, globalThis: *JSC.JSGlobalObject) bun.JSError!?bun.String { + if (value.isString()) { + return try bun.String.fromJS(value, globalThis); + } + + const out = try JSC.URL.hrefFromJS(value, globalThis); + if (out.tag == .Dead) return null; + return out; + } +}; + +comptime { + const Bun__fetch = JSC.toJSHostFunction(Bun__fetch_); + @export(&Bun__fetch, .{ .name = "Bun__fetch" }); +} + +/// Implementation of `Bun.fetch` +pub fn Bun__fetch_( + ctx: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSC.JSValue { + JSC.markBinding(@src()); + const globalThis = ctx; + const arguments = callframe.arguments_old(2); + bun.Analytics.Features.fetch += 1; + const vm = JSC.VirtualMachine.get(); + + var memory_reporter = bun.default_allocator.create(JSC.MemoryReportingAllocator) catch bun.outOfMemory(); + // used to clean up dynamically allocated memory on error (a poor man's errdefer) + var is_error = false; + var allocator = memory_reporter.wrap(bun.default_allocator); + errdefer bun.default_allocator.destroy(memory_reporter); + defer { + memory_reporter.report(globalThis.vm()); + + if (is_error) 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 args = JSC.Node.ArgumentsSlice.init(vm, arguments.slice()); + + 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: FetchTasklet.HTTPRequestBody = FetchTasklet.HTTPRequestBody.Empty; + + var disable_timeout = false; + var disable_keepalive = false; + var disable_decompression = false; + var verbose: http.HTTPVerboseLevel = if (vm.log.level.atLeast(.debug)) .headers else .none; + if (verbose == .none) { + verbose = vm.getVerboseFetch(); + } + + var proxy: ?ZigURL = null; + var redirect_type: FetchRedirect = FetchRedirect.follow; + var signal: ?*JSC.WebCore.AbortSignal = null; + // Custom Hostname + var hostname: ?[]u8 = null; + var range: ?[]u8 = null; + var unix_socket_path: ZigString.Slice = ZigString.Slice.empty; + + var url_proxy_buffer: []const u8 = ""; + const URLType = enum { + remote, + file, + blob, + }; + var url_type = URLType.remote; + + var ssl_config: ?*SSLConfig = null; + var reject_unauthorized = vm.getTLSRejectUnauthorized(); + var check_server_identity: JSValue = .zero; + + defer { + if (signal) |sig| { + signal = null; + sig.unref(); + } + + unix_socket_path.deinit(); + + allocator.free(url_proxy_buffer); + url_proxy_buffer = ""; + + if (headers) |*headers_| { + headers_.buf.deinit(allocator); + headers_.entries.deinit(allocator); + headers = null; + } + + body.detach(); + + // clean hostname if any + if (hostname) |hn| { + bun.default_allocator.free(hn); + hostname = null; + } + if (range) |range_| { + bun.default_allocator.free(range_); + range = null; + } + + if (ssl_config) |conf| { + ssl_config = null; + conf.deinit(); + bun.default_allocator.destroy(conf); + } + } + + const options_object: ?JSValue = brk: { + if (args.nextEat()) |options| { + if (options.isObject() or options.jsType() == .DOMWrapper) { + break :brk options; + } + } + + break :brk null; + }; + const request: ?*Request = brk: { + if (first_arg.isCell()) { + if (first_arg.asDirect(Request)) |request_| { + break :brk request_; + } + } + + break :brk null; + }; + // If it's NOT a Request or a subclass of Request, treat the first argument as a URL. + const url_str_optional = if (first_arg.as(Request) == null) try StringOrURL.fromJS(first_arg, globalThis) else null; + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + const request_init_object: ?JSValue = brk: { + if (request != null) break :brk null; + if (url_str_optional != null) break :brk null; + if (first_arg.isObject()) break :brk first_arg; + break :brk null; + }; + + var url_str = extract_url: { + if (url_str_optional) |str| break :extract_url str; + + if (request) |req| { + req.ensureURL() catch bun.outOfMemory(); + break :extract_url req.url.dupeRef(); + } + + if (request_init_object) |request_init| { + if (request_init.fastGet(globalThis, .url)) |url_| { + if (!url_.isUndefined()) { + break :extract_url try bun.String.fromJS(url_, globalThis); + } + } + } + + break :extract_url bun.String.empty; + }; + defer url_str.deref(); + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + if (url_str.isEmpty()) { + is_error = true; + const err = JSC.toTypeError(.ERR_INVALID_URL, fetch_error_blank_url, .{}, ctx); + return JSPromise.rejectedPromiseValue(globalThis, err); + } + + if (url_str.hasPrefixComptime("data:")) { + var url_slice = url_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", .{}); + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, err); + }; + + data_url.url = url_str; + return dataURLResponse(data_url, globalThis, allocator); + } + + url = ZigURL.fromString(allocator, url_str) catch { + const err = JSC.toTypeError(.ERR_INVALID_URL, "fetch() URL is invalid", .{}, ctx); + is_error = true; + return JSPromise.rejectedPromiseValue( + globalThis, + err, + ); + }; + if (url.isFile()) { + url_type = URLType.file; + } else if (url.isBlob()) { + url_type = URLType.blob; + } + url_proxy_buffer = url.href; + + if (url_str.hasPrefixComptime("data:")) { + var url_slice = url_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 = url_str; + + return dataURLResponse(data_url, globalThis, allocator); + } + + // **Start with the harmless ones.** + + // "method" + method = extract_method: { + if (options_object) |options| { + if (try options.getTruthyComptime(globalThis, "method")) |method_| { + break :extract_method try Method.fromJS(globalThis, method_); + } + } + + if (request) |req| { + break :extract_method req.method; + } + + if (request_init_object) |req| { + if (try req.getTruthyComptime(globalThis, "method")) |method_| { + break :extract_method try Method.fromJS(globalThis, method_); + } + } + + break :extract_method null; + } orelse .GET; + + // "decompress: boolean" + disable_decompression = extract_disable_decompression: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "decompress")) |decompression_value| { + if (decompression_value.isBoolean()) { + break :extract_disable_decompression !decompression_value.asBoolean(); + } else if (decompression_value.isNumber()) { + break :extract_disable_decompression decompression_value.to(i32) == 0; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + + break :extract_disable_decompression disable_decompression; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // "tls: TLSConfig" + ssl_config = extract_ssl_config: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "tls")) |tls| { + if (tls.isObject()) { + if (try tls.get(ctx, "rejectUnauthorized")) |reject| { + if (reject.isBoolean()) { + reject_unauthorized = reject.asBoolean(); + } else if (reject.isNumber()) { + reject_unauthorized = reject.to(i32) != 0; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + if (try tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| { + if (checkServerIdentity.isCell() and checkServerIdentity.isCallable()) { + check_server_identity = checkServerIdentity; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + if (SSLConfig.fromJS(vm, globalThis, tls) catch { + is_error = true; + return .zero; + }) |config| { + const ssl_config_object = bun.default_allocator.create(SSLConfig) catch bun.outOfMemory(); + ssl_config_object.* = config; + break :extract_ssl_config ssl_config_object; + } + } + } + } + } + + break :extract_ssl_config ssl_config; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // unix: string | undefined + unix_socket_path = extract_unix_socket_path: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "unix")) |socket_path| { + if (socket_path.isString() and socket_path.getLength(ctx) > 0) { + if (socket_path.toSliceCloneWithAllocator(globalThis, allocator)) |slice| { + break :extract_unix_socket_path slice; + } + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + break :extract_unix_socket_path unix_socket_path; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // timeout: false | number | undefined + disable_timeout = extract_disable_timeout: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "timeout")) |timeout_value| { + if (timeout_value.isBoolean()) { + break :extract_disable_timeout !timeout_value.asBoolean(); + } else if (timeout_value.isNumber()) { + break :extract_disable_timeout timeout_value.to(i32) == 0; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + + break :extract_disable_timeout disable_timeout; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // redirect: "follow" | "error" | "manual" | undefined; + redirect_type = extract_redirect_type: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (objects_to_try[i].getOptionalEnum(globalThis, "redirect", FetchRedirect) catch { + is_error = true; + return .zero; + }) |redirect_value| { + break :extract_redirect_type redirect_value; + } + } + } + + break :extract_redirect_type redirect_type; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // keepalive: boolean | undefined; + disable_keepalive = extract_disable_keepalive: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "keepalive")) |keepalive_value| { + if (keepalive_value.isBoolean()) { + break :extract_disable_keepalive !keepalive_value.asBoolean(); + } else if (keepalive_value.isNumber()) { + break :extract_disable_keepalive keepalive_value.to(i32) == 0; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + + break :extract_disable_keepalive disable_keepalive; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // verbose: boolean | "curl" | undefined; + verbose = extract_verbose: { + const objects_to_try = [_]JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "verbose")) |verb| { + if (verb.isString()) { + if ((try verb.getZigString(globalThis)).eqlComptime("curl")) { + break :extract_verbose .curl; + } + } else if (verb.isBoolean()) { + break :extract_verbose if (verb.toBoolean()) .headers else .none; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + break :extract_verbose verbose; + }; + + // proxy: string | undefined; + url_proxy_buffer = extract_proxy: { + const objects_to_try = [_]JSC.JSValue{ + options_object orelse .zero, + request_init_object orelse .zero, + }; + inline for (0..2) |i| { + if (objects_to_try[i] != .zero) { + if (try objects_to_try[i].get(globalThis, "proxy")) |proxy_arg| { + if (proxy_arg.isString() and proxy_arg.getLength(ctx) > 0) { + var href = try JSC.URL.hrefFromJS(proxy_arg, globalThis); + if (href.tag == .Dead) { + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, err); + } + defer href.deref(); + const buffer = try std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }); + url = ZigURL.parse(buffer[0..url.href.len]); + if (url.isFile()) { + url_type = URLType.file; + } else if (url.isBlob()) { + url_type = URLType.blob; + } + + proxy = ZigURL.parse(buffer[url.href.len..]); + allocator.free(url_proxy_buffer); + break :extract_proxy buffer; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + } + + break :extract_proxy url_proxy_buffer; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // signal: AbortSignal | undefined; + signal = extract_signal: { + if (options_object) |options| { + if (try options.get(globalThis, "signal")) |signal_| { + if (!signal_.isUndefined()) { + if (signal_.as(JSC.WebCore.AbortSignal)) |signal__| { + break :extract_signal signal__.ref(); + } + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + + if (request) |req| { + if (req.signal) |signal_| { + break :extract_signal signal_.ref(); + } + break :extract_signal null; + } + + if (request_init_object) |options| { + if (try options.get(globalThis, "signal")) |signal_| { + if (signal_.isUndefined()) { + break :extract_signal null; + } + + if (signal_.as(JSC.WebCore.AbortSignal)) |signal__| { + break :extract_signal signal__.ref(); + } + } + } + + break :extract_signal null; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // We do this 2nd to last instead of last so that if it's a FormData + // object, we can still insert the boundary. + // + // body: BodyInit | null | undefined; + // + body = extract_body: { + if (options_object) |options| { + if (options.fastGet(globalThis, .body)) |body__| { + if (!body__.isUndefined()) { + break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + + if (request) |req| { + if (req.body.value == .Used or (req.body.value == .Locked and (req.body.value.Locked.action != .none or req.body.value.Locked.isDisturbed(Request, globalThis, first_arg)))) { + return globalThis.ERR_BODY_ALREADY_USED("Request body already used", .{}).throw(); + } + + if (req.body.value == .Locked) { + if (req.body.value.Locked.readable.has()) { + break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get(globalThis).?, globalThis) }; + } + const readable = req.body.value.toReadableStream(globalThis); + if (!readable.isEmptyOrUndefinedOrNull() and req.body.value == .Locked and req.body.value.Locked.readable.has()) { + break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get(globalThis).?, globalThis) }; + } + } + + break :extract_body FetchTasklet.HTTPRequestBody{ .AnyBlob = req.body.value.useAsAnyBlob() }; + } + + if (request_init_object) |req| { + if (req.fastGet(globalThis, .body)) |body__| { + if (!body__.isUndefined()) { + break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); + } + } + } + + break :extract_body null; + } orelse FetchTasklet.HTTPRequestBody.Empty; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // headers: Headers | undefined; + headers = extract_headers: { + var fetch_headers_to_deref: ?*JSC.FetchHeaders = null; + defer { + if (fetch_headers_to_deref) |fetch_headers| { + fetch_headers.deref(); + } + } + + const fetch_headers: ?*JSC.FetchHeaders = brk: { + if (options_object) |options| { + if (options.fastGet(globalThis, .headers)) |headers_value| { + if (!headers_value.isUndefined()) { + if (headers_value.as(FetchHeaders)) |headers__| { + if (headers__.isEmpty()) { + break :brk null; + } + + break :brk headers__; + } + + if (FetchHeaders.createFromJS(ctx, headers_value)) |headers__| { + fetch_headers_to_deref = headers__; + break :brk headers__; + } + + break :brk null; + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + } + + if (request) |req| { + if (req.getFetchHeadersUnlessEmpty()) |head| { + break :brk head; + } + + break :brk null; + } + + if (request_init_object) |options| { + if (options.fastGet(globalThis, .headers)) |headers_value| { + if (!headers_value.isUndefined()) { + if (headers_value.as(FetchHeaders)) |headers__| { + if (headers__.isEmpty()) { + break :brk null; + } + + break :brk headers__; + } + + if (FetchHeaders.createFromJS(ctx, headers_value)) |headers__| { + fetch_headers_to_deref = headers__; + break :brk headers__; + } + + break :brk null; + } + } + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + break :extract_headers headers; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + if (fetch_headers) |headers_| { + if (headers_.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { + if (hostname) |host| { + hostname = null; + allocator.free(host); + } + hostname = _hostname.toOwnedSliceZ(allocator) catch bun.outOfMemory(); + } + if (url.isS3()) { + if (headers_.fastGet(JSC.FetchHeaders.HTTPHeaderName.Range)) |_range| { + if (range) |range_| { + range = null; + allocator.free(range_); + } + range = _range.toOwnedSliceZ(allocator) catch bun.outOfMemory(); + } + } + + break :extract_headers Headers.from(headers_, allocator, .{ .body = body.getAnyBlob() }) catch bun.outOfMemory(); + } + + break :extract_headers headers; + }; + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + if (proxy != null and unix_socket_path.length() > 0) { + is_error = true; + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_proxy_unix, .{}, ctx); + return JSPromise.rejectedPromiseValue(globalThis, err); + } + + if (globalThis.hasException()) { + is_error = true; + return .zero; + } + + // 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 (url_type != .remote) { + defer unix_socket_path.deinit(); + var path_buf: bun.PathBuffer = undefined; + const PercentEncoding = @import("../../url.zig").PercentEncoding; + var path_buf2: bun.PathBuffer = undefined; + var stream = std.io.fixedBufferStream(&path_buf2); + var url_path_decoded = path_buf2[0 .. PercentEncoding.decode( + @TypeOf(&stream.writer()), + &stream.writer(), + switch (url_type) { + .file => url.path, + .blob => url.href["blob:".len..], + .remote => unreachable, + }, + ) catch |err| { + return globalThis.throwError(err, "Failed to decode file url"); + }]; + var url_string: bun.String = bun.String.empty; + defer url_string.deref(); + // This can be a blob: url or a file: url. + const blob_to_use = blob: { + + // Support blob: urls + if (url_type == URLType.blob) { + if (JSC.WebCore.ObjectURLRegistry.singleton().resolveAndDupe(url_path_decoded)) |blob| { + url_string = bun.String.createFormat("blob:{s}", .{url_path_decoded}) catch bun.outOfMemory(); + break :blob blob; + } else { + // Consistent with what Node.js does - it rejects, not a 404. + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "Failed to resolve blob:{s}", .{ + url_path_decoded, + }, ctx); + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, err); + } + } + + const temp_file_path = brk: { + if (std.fs.path.isAbsolute(url_path_decoded)) { + if (Environment.isWindows) { + // pathname will start with / if is a absolute path on windows, so we remove before normalizing it + if (url_path_decoded[0] == '/') { + url_path_decoded = url_path_decoded[1..]; + } + break :brk PosixToWinNormalizer.resolveCWDWithExternalBufZ(&path_buf, url_path_decoded) catch |err| { + return globalThis.throwError(err, "Failed to resolve file url"); + }; + } + break :brk url_path_decoded; + } + + var cwd_buf: bun.PathBuffer = undefined; + const cwd = if (Environment.isWindows) (bun.getcwd(&cwd_buf) catch |err| { + return globalThis.throwError(err, "Failed to resolve file url"); + }) else globalThis.bunVM().transpiler.fs.top_level_dir; + + const fullpath = bun.path.joinAbsStringBuf( + cwd, + &path_buf, + &[_]string{ + globalThis.bunVM().main, + "../", + url_path_decoded, + }, + .auto, + ); + if (Environment.isWindows) { + break :brk PosixToWinNormalizer.resolveCWDWithExternalBufZ(&path_buf2, fullpath) catch |err| { + return globalThis.throwError(err, "Failed to resolve file url"); + }; + } + break :brk fullpath; + }; + + url_string = JSC.URL.fileURLFromString(bun.String.fromUTF8(temp_file_path)); + + var pathlike: JSC.Node.PathOrFileDescriptor = .{ + .path = .{ + .encoded_slice = ZigString.Slice.init(bun.default_allocator, try bun.default_allocator.dupe(u8, temp_file_path)), + }, + }; + + break :blob Blob.findOrCreateFileFromPath( + &pathlike, + globalThis, + true, + ); + }; + + const response = bun.new(Response, Response{ + .body = Body{ + .value = .{ .Blob = blob_to_use }, + }, + .init = Response.Init{ + .status_code = 200, + }, + .url = url_string.clone(), + }); + + return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); + } + + if (url.protocol.len > 0) { + if (!(url.isHTTP() or url.isHTTPS() or url.isS3())) { + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http:, https: or s3:", .{}, ctx); + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, err); + } + } + + if (!method.hasRequestBody() and body.hasBody()) { + const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, err); + } + + if (headers == null and body.hasBody() and body.hasContentTypeFromUser()) { + headers = Headers.from( + null, + allocator, + .{ .body = body.getAnyBlob() }, + ) catch bun.outOfMemory(); + } + + var http_body = body; + if (body.isS3()) { + prepare_body: { + // is a S3 file we can use chunked here + + if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob(globalThis, &body.AnyBlob.Blob, s3.MultiPartUploadOptions.DefaultPartSize), globalThis)) |stream| { + var old = body; + defer old.detach(); + body = .{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis) }; + break :prepare_body; + } + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Failed to start s3 stream", .{})); + body.detach(); + + return rejected_value; + } + } + if (body.needsToReadFile()) { + prepare_body: { + const opened_fd_res: JSC.Maybe(bun.FileDescriptor) = switch (body.store().?.data.file.pathlike) { + .fd => |fd| bun.sys.dup(fd), + .path => |path| bun.sys.open(path.sliceZ(&globalThis.bunVM().nodeFS().sync_error_buf), if (Environment.isWindows) bun.O.RDONLY else bun.O.RDONLY | bun.O.NOCTTY, 0), + }; + + const opened_fd = switch (opened_fd_res) { + .err => |err| { + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + is_error = 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.AnyBlob.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.AnyBlob.Blob.offset + original_size, + .offset = body.AnyBlob.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.AnyBlob.Blob.offset, + .max_size = body.AnyBlob.Blob.size, + }, + .sync, + ); + + if (body.store().?.data.file.pathlike == .path) { + _ = bun.sys.close(opened_fd); + } + + switch (res) { + .err => |err| { + is_error = true; + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + body.detach(); + + return rejected_value; + }, + .result => |result| { + body.detach(); + body = .{ .AnyBlob = .fromOwnedSlice(allocator, @constCast(result.slice())) }; + http_body = .{ .AnyBlob = body.AnyBlob }; + }, + } + } + } + + if (url.isS3()) { + // get ENV config + var credentialsWithOptions: s3.S3CredentialsWithOptions = .{ + .credentials = globalThis.bunVM().transpiler.env.getS3Credentials(), + .options = .{}, + .acl = null, + .storage_class = null, + }; + defer { + credentialsWithOptions.deinit(); + } + + if (options_object) |options| { + if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| { + if (s3_options.isObject()) { + s3_options.ensureStillAlive(); + credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, null, globalThis); + } + } + } + + if (body == .ReadableStream) { + // we cannot direct stream to s3 we need to use multi part upload + defer body.ReadableStream.deinit(); + const Wrapper = struct { + promise: JSC.JSPromise.Strong, + url: ZigURL, + url_proxy_buffer: []const u8, + global: *JSC.JSGlobalObject, + + pub usingnamespace bun.New(@This()); + + pub fn resolve(result: s3.S3UploadResult, self: *@This()) void { + const global = self.global; + switch (result) { + .success => { + const response = bun.new(Response, Response{ + .body = .{ .value = .Empty }, + .redirected = false, + .init = .{ .method = .PUT, .status_code = 200 }, + .url = bun.String.createAtomIfPossible(self.url.href), + }); + const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); + response_js.ensureStillAlive(); + self.promise.resolve(global, response_js); + }, + .failure => |err| { + const response = bun.new(Response, Response{ + .body = .{ + .value = .{ + .InternalBlob = .{ + .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, bun.default_allocator.dupe(u8, err.message) catch bun.outOfMemory()), + .was_string = true, + }, + }, + }, + .redirected = false, + .init = .{ + .method = .PUT, + .status_code = 500, + .status_text = bun.String.createAtomIfPossible(err.code), + }, + .url = bun.String.createAtomIfPossible(self.url.href), + }); + const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); + response_js.ensureStillAlive(); + self.promise.resolve(global, response_js); + }, + } + bun.default_allocator.free(self.url_proxy_buffer); + self.destroy(); + } + }; + if (method != .PUT and method != .POST) { + return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Only POST and PUT do support body when using S3", .{})); + } + const promise = JSC.JSPromise.Strong.init(globalThis); + + const s3_stream = Wrapper.new(.{ + .url = url, + .url_proxy_buffer = url_proxy_buffer, + .promise = promise, + .global = globalThis, + }); + + const promise_value = promise.value(); + const proxy_url = if (proxy) |p| p.href else ""; + _ = bun.S3.uploadStream( + credentialsWithOptions.credentials.dupe(), + url.s3Path(), + body.ReadableStream.get(globalThis).?, + globalThis, + credentialsWithOptions.options, + credentialsWithOptions.acl, + credentialsWithOptions.storage_class, + if (headers) |h| h.getContentType() else null, + proxy_url, + @ptrCast(&Wrapper.resolve), + s3_stream, + ); + url = .{}; + url_proxy_buffer = ""; + return promise_value; + } + if (method == .POST) { + method = .PUT; + } + + var result = credentialsWithOptions.credentials.signRequest(.{ + .path = url.s3Path(), + .method = method, + }, null) catch |sign_err| { + is_error = true; + return JSPromise.rejectedPromiseValue(globalThis, s3.getJSSignError(sign_err, globalThis)); + }; + defer result.deinit(); + if (proxy) |proxy_| { + // proxy and url are in the same buffer lets replace it + const old_buffer = url_proxy_buffer; + defer allocator.free(old_buffer); + var buffer = allocator.alloc(u8, result.url.len + proxy_.href.len) catch bun.outOfMemory(); + bun.copy(u8, buffer[0..result.url.len], result.url); + bun.copy(u8, buffer[proxy_.href.len..], proxy_.href); + url_proxy_buffer = buffer; + + url = ZigURL.parse(url_proxy_buffer[0..result.url.len]); + proxy = ZigURL.parse(url_proxy_buffer[result.url.len..]); + } else { + // replace headers and url of the request + allocator.free(url_proxy_buffer); + url_proxy_buffer = result.url; + url = ZigURL.parse(result.url); + result.url = ""; // fetch now owns this + } + + const content_type = if (headers) |h| h.getContentType() else null; + var header_buffer: [10]picohttp.Header = undefined; + + if (range) |range_| { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); + setHeaders(&headers, _headers, allocator); + } else if (content_type) |ct| { + if (ct.len > 0) { + const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = ct }); + setHeaders(&headers, _headers, allocator); + } else { + setHeaders(&headers, result.headers(), allocator); + } + } else { + setHeaders(&headers, result.headers(), allocator); + } + } + + // 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(); + + const initial_body_reference_count: if (Environment.isDebug) usize else u0 = brk: { + if (Environment.isDebug) { + if (body.store()) |store| { + break :brk store.ref_count.load(.monotonic); + } + } + + break :brk 0; + }; + + _ = FetchTasklet.queue( + allocator, + globalThis, + .{ + .method = method, + .url = url, + .headers = headers orelse Headers{ + .allocator = allocator, + }, + .body = http_body, + .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, + .ssl_config = ssl_config, + .hostname = hostname, + .memory_reporter = memory_reporter, + .check_server_identity = if (check_server_identity.isEmptyOrUndefinedOrNull()) .empty else .create(check_server_identity, globalThis), + .unix_socket_path = unix_socket_path, + }, + // 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 bun.outOfMemory(); + + if (Environment.isDebug) { + if (body.store()) |store| { + if (store.ref_count.load(.monotonic) == initial_body_reference_count) { + Output.panic("Expected body ref count to have incremented in FetchTasklet", .{}); + } + } + } + + // These are now owned by FetchTasklet. + url = .{}; + headers = null; + // Reference count for the blob is incremented above. + if (body.store() != null) { + body.detach(); + } else { + // These are single-use, and have effectively been moved to the FetchTasklet. + body = FetchTasklet.HTTPRequestBody.Empty; + } + proxy = null; + url_proxy_buffer = ""; + signal = null; + ssl_config = null; + hostname = null; + unix_socket_path = ZigString.Slice.empty; + + return promise_val; +} +fn setHeaders(headers: *?Headers, new_headers: []const picohttp.Header, allocator: std.mem.Allocator) void { + var old = headers.*; + headers.* = Headers.fromPicoHttpHeaders(new_headers, allocator) catch bun.outOfMemory(); + + if (old) |*headers_| { + headers_.deinit(); + } +} +const std = @import("std"); +const bun = @import("root").bun; +const JSC = bun.JSC; +const js = JSC.C; +const DataURL = @import("../../resolver/data_url.zig").DataURL; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const ZigString = JSC.ZigString; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSPromise = JSC.JSPromise; +const ZigURL = @import("../../url.zig").URL; +const Output = bun.Output; +const http = bun.http; +const VirtualMachine = JSC.VirtualMachine; +const FetchRedirect = http.FetchRedirect; +const Blob = JSC.WebCore.Blob; +const Response = JSC.WebCore.Response; +const Request = JSC.WebCore.Request; +const Headers = JSC.WebCore.Headers; +const Method = @import("../../http/method.zig").Method; +const Body = JSC.WebCore.Body; +const Async = bun.Async; +const SSLConfig = @import("../api/server.zig").ServerConfig.SSLConfig; +const Mutex = bun.Mutex; +const BoringSSL = bun.BoringSSL.c; +const X509 = @import("../api/bun/x509.zig"); +const FetchHeaders = JSC.FetchHeaders; +const Environment = bun.Environment; +const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; +const AnyBlob = JSC.WebCore.AnyBlob; +const s3 = bun.S3; +const picohttp = bun.picohttp; diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 7d189c187f..a43eecc543 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -774,2743 +774,6 @@ pub const Response = struct { } }; -const null_fd = bun.invalid_fd; -fn setHeaders(headers: *?Headers, new_headers: []const picohttp.Header, allocator: std.mem.Allocator) void { - var old = headers.*; - headers.* = Headers.fromPicoHttpHeaders(new_headers, allocator) catch bun.outOfMemory(); - - if (old) |*headers_| { - headers_.deinit(); - } -} -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."; - pub const fetch_error_proxy_unix = "fetch() cannot use a proxy with a unix socket."; - 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 FetchTasklet = struct { - pub const FetchTaskletStream = JSC.WebCore.NetworkSink; - - const log = Output.scoped(.FetchTasklet, false); - sink: ?*FetchTaskletStream.JSSink = null, - http: ?*http.AsyncHTTP = null, - result: http.HTTPClientResult = .{}, - metadata: ?http.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 weak ref we need this to track the response JS lifetime - response: JSC.Weak(FetchTasklet) = .{}, - /// native response ref if we still need it when JS is discarted - native_response: ?*Response = null, - ignore_data: bool = false, - /// 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: Async.KeepAlive = .{}, - 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: http.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: http.Signals = .{}, - signal_store: http.Signals.Store = .{}, - has_schedule_callback: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), - - // must be stored because AbortSignal stores reason weakly - abort_reason: JSC.Strong = .empty, - - // custom checkServerIdentity - check_server_identity: JSC.Strong = .empty, - reject_unauthorized: bool = true, - // Custom Hostname - hostname: ?[]u8 = null, - is_waiting_body: bool = false, - is_waiting_abort: bool = false, - is_waiting_request_stream_start: bool = false, - mutex: Mutex, - - tracker: JSC.AsyncTaskTracker, - - ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), - - pub fn ref(this: *FetchTasklet) void { - const count = this.ref_count.fetchAdd(1, .monotonic); - bun.debugAssert(count > 0); - } - - pub fn deref(this: *FetchTasklet) void { - const count = this.ref_count.fetchSub(1, .monotonic); - bun.debugAssert(count > 0); - - if (count == 1) { - this.deinit(); - } - } - - pub fn derefFromThread(this: *FetchTasklet) void { - const count = this.ref_count.fetchSub(1, .monotonic); - bun.debugAssert(count > 0); - - if (count == 1) { - // this is really unlikely to happen, but can happen - // lets make sure that we always call deinit from main thread - - this.javascript_vm.eventLoop().enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(this, FetchTasklet.deinit)); - } - } - - pub const HTTPRequestBody = union(enum) { - AnyBlob: AnyBlob, - Sendfile: http.Sendfile, - ReadableStream: JSC.WebCore.ReadableStream.Strong, - - pub const Empty: HTTPRequestBody = .{ .AnyBlob = .{ .Blob = .{} } }; - - pub fn store(this: *HTTPRequestBody) ?*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(), - .ReadableStream => |*stream| { - stream.deinit(); - }, - .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 fromJS(globalThis: *JSGlobalObject, value: JSValue) bun.JSError!HTTPRequestBody { - var body_value = try Body.Value.fromJS(globalThis, value); - if (body_value == .Used or (body_value == .Locked and (body_value.Locked.action != .none or body_value.Locked.isDisturbed2(globalThis)))) { - return globalThis.ERR_BODY_ALREADY_USED("body already used", .{}).throw(); - } - if (body_value == .Locked) { - if (body_value.Locked.readable.has()) { - // just grab the ref - return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; - } - const readable = body_value.toReadableStream(globalThis); - if (!readable.isEmptyOrUndefinedOrNull() and body_value == .Locked and body_value.Locked.readable.has()) { - return FetchTasklet.HTTPRequestBody{ .ReadableStream = body_value.Locked.readable }; - } - } - return FetchTasklet.HTTPRequestBody{ .AnyBlob = body_value.useAsAnyBlob() }; - } - - pub fn needsToReadFile(this: *HTTPRequestBody) bool { - return switch (this.*) { - .AnyBlob => |blob| blob.needsToReadFile(), - else => false, - }; - } - - pub fn isS3(this: *const HTTPRequestBody) bool { - return switch (this.*) { - .AnyBlob => |*blob| blob.isS3(), - else => false, - }; - } - - pub fn hasContentTypeFromUser(this: *HTTPRequestBody) bool { - return switch (this.*) { - .AnyBlob => |blob| blob.hasContentTypeFromUser(), - else => false, - }; - } - - pub fn getAnyBlob(this: *HTTPRequestBody) ?*AnyBlob { - return switch (this.*) { - .AnyBlob => &this.AnyBlob, - else => null, - }; - } - - pub fn hasBody(this: *HTTPRequestBody) bool { - return switch (this.*) { - .AnyBlob => |blob| blob.size() > 0, - .ReadableStream => |*stream| stream.has(), - .Sendfile => true, - }; - } - }; - - pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { - return FetchTasklet{}; - } - - fn clearSink(this: *FetchTasklet) void { - if (this.sink) |wrapper| { - this.sink = null; - - wrapper.sink.done = true; - wrapper.sink.ended = true; - wrapper.sink.finalize(); - wrapper.detach(); - wrapper.sink.finalizeAndDestroy(); - } - } - - 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; - } - - if (this.result.certificate_info) |*certificate| { - certificate.deinit(bun.default_allocator); - this.result.certificate_info = null; - } - - this.request_headers.entries.deinit(allocator); - this.request_headers.buf.deinit(allocator); - this.request_headers = Headers{ .allocator = undefined }; - - if (this.http) |http_| { - http_.clearData(); - } - - if (this.metadata != null) { - this.metadata.?.deinit(allocator); - this.metadata = null; - } - - this.response_buffer.deinit(); - this.response.deinit(); - if (this.native_response) |response| { - this.native_response = null; - - response.unref(); - } - - this.readable_stream_ref.deinit(); - - this.scheduled_response_buffer.deinit(); - if (this.request_body != .ReadableStream or this.is_waiting_request_stream_start) { - this.request_body.detach(); - } - - this.abort_reason.deinit(); - this.check_server_identity.deinit(); - this.clearAbortSignal(); - } - - pub fn deinit(this: *FetchTasklet) void { - log("deinit", .{}); - - bun.assert(this.ref_count.load(.monotonic) == 0); - - this.clearData(); - - var reporter = this.memory_reporter; - const allocator = reporter.allocator(); - - if (this.http) |http_| { - this.http = null; - allocator.destroy(http_); - } - allocator.destroy(this); - // reporter.assert(); - bun.default_allocator.destroy(reporter); - } - - fn getCurrentResponse(this: *FetchTasklet) ?*Response { - // we need a body to resolve the promise when buffering - if (this.native_response) |response| { - return response; - } - - // if we did not have a direct reference we check if the Weak ref is still alive - if (this.response.get()) |response_js| { - if (response_js.as(Response)) |response| { - return response; - } - } - - return null; - } - - pub fn onResolveRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - var args = callframe.arguments_old(2); - var this: *@This() = args.ptr[args.len - 1].asPromisePtr(@This()); - defer this.deref(); - if (this.request_body == .ReadableStream) { - var readable_stream_ref = this.request_body.ReadableStream; - this.request_body.ReadableStream = .{}; - defer readable_stream_ref.deinit(); - if (readable_stream_ref.get(globalThis)) |stream| { - stream.done(globalThis); - this.clearSink(); - } - } - - return JSValue.jsUndefined(); - } - - pub fn onRejectRequestStream(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const args = callframe.arguments_old(2); - var this = args.ptr[args.len - 1].asPromisePtr(@This()); - defer this.deref(); - const err = args.ptr[0]; - if (this.request_body == .ReadableStream) { - var readable_stream_ref = this.request_body.ReadableStream; - this.request_body.ReadableStream = .{}; - defer readable_stream_ref.deinit(); - if (readable_stream_ref.get(globalThis)) |stream| { - stream.cancel(globalThis); - this.clearSink(); - } - } - - this.abortListener(err); - return JSValue.jsUndefined(); - } - comptime { - const jsonResolveRequestStream = JSC.toJSHostFunction(onResolveRequestStream); - @export(&jsonResolveRequestStream, .{ .name = "Bun__FetchTasklet__onResolveRequestStream" }); - const jsonRejectRequestStream = JSC.toJSHostFunction(onRejectRequestStream); - @export(&jsonRejectRequestStream, .{ .name = "Bun__FetchTasklet__onRejectRequestStream" }); - } - - pub fn startRequestStream(this: *FetchTasklet) void { - this.is_waiting_request_stream_start = false; - bun.assert(this.request_body == .ReadableStream); - if (this.request_body.ReadableStream.get(this.global_this)) |stream| { - this.ref(); // lets only unref when sink is done - - const globalThis = this.global_this; - var response_stream = FetchTaskletStream.new(.{ - .task = .{ .fetch = this }, - .buffer = .{}, - .globalThis = globalThis, - }).toSink(); - var signal = &response_stream.sink.signal; - this.sink = response_stream; - - signal.* = FetchTaskletStream.JSSink.SinkSignal.init(JSValue.zero); - - // explicitly set it to a dead pointer - // we use this memory address to disable signals being sent - signal.clear(); - bun.assert(signal.isDead()); - - // We are already corked! - const assignment_result: JSValue = FetchTaskletStream.JSSink.assignToStream( - globalThis, - stream.value, - response_stream, - @as(**anyopaque, @ptrCast(&signal.ptr)), - ); - - assignment_result.ensureStillAlive(); - - // assert that it was updated - bun.assert(!signal.isDead()); - - if (assignment_result.toError()) |err_value| { - response_stream.detach(); - this.sink = null; - response_stream.sink.finalizeAndDestroy(); - return this.abortListener(err_value); - } - - if (!assignment_result.isEmptyOrUndefinedOrNull()) { - assignment_result.ensureStillAlive(); - // it returns a Promise when it goes through ReadableStreamDefaultReader - if (assignment_result.asAnyPromise()) |promise| { - switch (promise.status(globalThis.vm())) { - .pending => { - this.ref(); - assignment_result.then( - globalThis, - this, - onResolveRequestStream, - onRejectRequestStream, - ); - }, - .fulfilled => { - var readable_stream_ref = this.request_body.ReadableStream; - this.request_body.ReadableStream = .{}; - defer { - stream.done(globalThis); - this.clearSink(); - readable_stream_ref.deinit(); - } - }, - .rejected => { - var readable_stream_ref = this.request_body.ReadableStream; - this.request_body.ReadableStream = .{}; - defer { - stream.cancel(globalThis); - this.clearSink(); - readable_stream_ref.deinit(); - } - - this.abortListener(promise.result(globalThis.vm())); - }, - } - return; - } else { - // if is not a promise we treat it as Error - response_stream.detach(); - this.sink = null; - response_stream.sink.finalizeAndDestroy(); - return this.abortListener(assignment_result); - } - } - } - } - pub fn onBodyReceived(this: *FetchTasklet) void { - const success = this.result.isSuccess(); - const globalThis = this.global_this; - // reset the buffer if we are streaming or if we are not waiting for bufferig anymore - var buffer_reset = true; - defer { - if (buffer_reset) { - this.scheduled_response_buffer.reset(); - } - } - - if (!success) { - var err = this.onReject(); - var need_deinit = true; - defer if (need_deinit) err.deinit(); - // if we are streaming update with error - if (this.readable_stream_ref.get(globalThis)) |readable| { - if (readable.ptr == .Bytes) { - readable.ptr.Bytes.onData( - .{ - .err = .{ .JSValue = err.toJS(globalThis) }, - }, - bun.default_allocator, - ); - } - } - // if we are buffering resolve the promise - if (this.getCurrentResponse()) |response| { - response.body.value.toErrorInstance(err, globalThis); - need_deinit = false; // body value now owns the error - const body = response.body; - if (body.value == .Locked) { - if (body.value.Locked.promise) |promise_| { - const promise = promise_.asAnyPromise().?; - promise.reject(globalThis, response.body.value.Error.toJS(globalThis)); - } - } - } - return; - } - - if (this.readable_stream_ref.get(globalThis)) |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 - const 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, - ); - } else { - var prev = this.readable_stream_ref; - this.readable_stream_ref = .{}; - defer prev.deinit(); - buffer_reset = false; - this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice()); - this.scheduled_response_buffer = .{ - .allocator = bun.default_allocator, - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }; - readable.ptr.Bytes.onData( - .{ - .owned_and_done = bun.ByteList.initConst(chunk), - }, - bun.default_allocator, - ); - } - return; - } - } - - if (this.getCurrentResponse()) |response| { - var body = &response.body; - if (body.value == .Locked) { - if (body.value.Locked.readable.get(globalThis)) |readable| { - if (readable.ptr == .Bytes) { - readable.ptr.Bytes.size_hint = this.getSizeHint(); - - const 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, - ); - } else { - var prev = body.value.Locked.readable; - body.value.Locked.readable = .{}; - readable.value.ensureStillAlive(); - prev.deinit(); - readable.value.ensureStillAlive(); - 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, this is also the only case we dont wanna to reset the buffer - buffer_reset = false; - 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; - const 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, response.getFetchHeaders()); - } - } - } - } - } - - pub fn onProgressUpdate(this: *FetchTasklet) void { - JSC.markBinding(@src()); - log("onProgressUpdate", .{}); - this.mutex.lock(); - this.has_schedule_callback.store(false, .monotonic); - const is_done = !this.result.has_more; - - const vm = this.javascript_vm; - // vm is shutting down we cannot touch JS - if (vm.isShuttingDown()) { - this.mutex.unlock(); - if (is_done) { - this.deref(); - } - return; - } - - const globalThis = this.global_this; - defer { - this.mutex.unlock(); - // if we are not done we wait until the next call - if (is_done) { - var poll_ref = this.poll_ref; - this.poll_ref = .{}; - poll_ref.unref(vm); - this.deref(); - } - } - if (this.is_waiting_request_stream_start and this.result.can_stream) { - // start streaming - this.startRequestStream(); - } - // if we already respond the metadata and still need to process the body - if (this.is_waiting_body) { - this.onBodyReceived(); - return; - } - if (this.metadata == null and this.result.isSuccess()) return; - - // if we abort because of cert error - // we wait the Http Client because we already have the response - // we just need to deinit - if (this.is_waiting_abort) { - return; - } - const promise_value = this.promise.valueOrEmpty(); - - if (promise_value.isEmptyOrUndefinedOrNull()) { - log("onProgressUpdate: promise_value is null", .{}); - this.promise.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; - var result = this.onReject(); - defer result.deinit(); - - promise_value.ensureStillAlive(); - promise.reject(globalThis, result.toJS(globalThis)); - - tracker.didDispatch(globalThis); - this.promise.deinit(); - return; - } - // everything ok - if (this.metadata == null) { - log("onProgressUpdate: metadata is null", .{}); - return; - } - } - - const tracker = this.tracker; - tracker.willDispatch(globalThis); - defer { - log("onProgressUpdate: promise_value is not null", .{}); - tracker.didDispatch(globalThis); - this.promise.deinit(); - } - const success = this.result.isSuccess(); - const result = switch (success) { - true => JSC.Strong.create(this.onResolve(), globalThis), - false => brk: { - // in this case we wanna a JSC.Strong so we just convert it - var value = this.onReject(); - _ = value.toJS(globalThis); - break :brk value.JSValue; - }, - }; - - promise_value.ensureStillAlive(); - const Holder = struct { - held: JSC.Strong, - promise: JSC.Strong, - globalObject: *JSC.JSGlobalObject, - task: JSC.AnyTask, - - pub fn resolve(self: *@This()) void { - // cleanup - defer bun.default_allocator.destroy(self); - defer self.held.deinit(); - defer self.promise.deinit(); - // resolve the promise - var prom = self.promise.swap().asAnyPromise().?; - const res = self.held.swap(); - res.ensureStillAlive(); - prom.resolve(self.globalObject, res); - } - - pub fn reject(self: *@This()) void { - // cleanup - defer bun.default_allocator.destroy(self); - defer self.held.deinit(); - defer self.promise.deinit(); - - // reject the promise - var prom = self.promise.swap().asAnyPromise().?; - const res = self.held.swap(); - res.ensureStillAlive(); - prom.reject(self.globalObject, res); - } - }; - var holder = bun.default_allocator.create(Holder) catch bun.outOfMemory(); - holder.* = .{ - .held = result, - // we need the promise to be alive until the task is done - .promise = this.promise.strong, - .globalObject = globalThis, - .task = undefined, - }; - this.promise.strong = .empty; - holder.task = switch (success) { - true => JSC.AnyTask.New(Holder, Holder.resolve).init(holder), - false => JSC.AnyTask.New(Holder, Holder.reject).init(holder), - }; - - vm.enqueueTask(JSC.Task.init(&holder.task)); - } - - pub fn checkServerIdentity(this: *FetchTasklet, certificate_info: http.CertificateInfo) bool { - if (this.check_server_identity.get()) |check_server_identity| { - check_server_identity.ensureStillAlive(); - if (certificate_info.cert.len > 0) { - const cert = certificate_info.cert; - var cert_ptr = cert.ptr; - if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| { - const globalObject = this.global_this; - defer x509.free(); - const js_cert = X509.toJS(x509, globalObject) catch |err| { - switch (err) { - error.JSError => {}, - error.OutOfMemory => globalObject.throwOutOfMemory() catch {}, - } - const check_result = globalObject.tryTakeException().?; - // mark to wait until deinit - this.is_waiting_abort = this.result.has_more; - this.abort_reason.set(globalObject, check_result); - this.signal_store.aborted.store(true, .monotonic); - this.tracker.didCancel(this.global_this); - // we need to abort the request - if (this.http) |http_| http.http_thread.scheduleShutdown(http_); - this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; - return false; - }; - var hostname: bun.String = bun.String.createUTF8(certificate_info.hostname); - defer hostname.deref(); - const js_hostname = hostname.toJS(globalObject); - js_hostname.ensureStillAlive(); - js_cert.ensureStillAlive(); - const check_result = check_server_identity.call(globalObject, .undefined, &.{ js_hostname, js_cert }) catch |err| globalObject.takeException(err); - - // > Returns object [...] on failure - if (check_result.isAnyError()) { - // mark to wait until deinit - this.is_waiting_abort = this.result.has_more; - this.abort_reason.set(globalObject, check_result); - this.signal_store.aborted.store(true, .monotonic); - this.tracker.didCancel(this.global_this); - - // we need to abort the request - if (this.http) |http_| { - http.http_thread.scheduleShutdown(http_); - } - this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; - return false; - } - - // > On success, returns - // We treat any non-error value as a success. - return true; - } - } - } - this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID; - return false; - } - - fn getAbortError(this: *FetchTasklet) ?Body.Value.ValueError { - if (this.abort_reason.has()) { - defer this.clearAbortSignal(); - const out = this.abort_reason; - - this.abort_reason = .empty; - return Body.Value.ValueError{ .JSValue = out }; - } - - if (this.signal) |signal| { - if (signal.reasonIfAborted(this.global_this)) |reason| { - defer this.clearAbortSignal(); - return reason.toBodyValueError(this.global_this); - } - } - - return null; - } - - fn clearAbortSignal(this: *FetchTasklet) void { - const signal = this.signal orelse return; - this.signal = null; - defer { - signal.pendingActivityUnref(); - signal.unref(); - } - - signal.cleanNativeBindings(this); - } - - pub fn onReject(this: *FetchTasklet) Body.Value.ValueError { - bun.assert(this.result.fail != null); - log("onReject", .{}); - - if (this.getAbortError()) |err| { - return err; - } - - if (this.result.abortReason()) |reason| { - return .{ .AbortReason = reason }; - } - - // some times we don't have metadata so we also check http.url - const path = if (this.metadata) |metadata| - bun.String.createUTF8(metadata.url) - else if (this.http) |http_| - bun.String.createUTF8(http_.url.href) - else - bun.String.empty; - - const fetch_error = JSC.SystemError{ - .code = bun.String.static(switch (this.result.fail.?) { - error.ConnectionClosed => "ECONNRESET", - else => |e| @errorName(e), - }), - .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?"), - error.RedirectURLInvalid => bun.String.static("Redirect URL in Location header is invalid."), - - error.UNABLE_TO_GET_ISSUER_CERT => bun.String.static("unable to get issuer certificate"), - error.UNABLE_TO_GET_CRL => bun.String.static("unable to get certificate CRL"), - error.UNABLE_TO_DECRYPT_CERT_SIGNATURE => bun.String.static("unable to decrypt certificate's signature"), - error.UNABLE_TO_DECRYPT_CRL_SIGNATURE => bun.String.static("unable to decrypt CRL's signature"), - error.UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY => bun.String.static("unable to decode issuer public key"), - error.CERT_SIGNATURE_FAILURE => bun.String.static("certificate signature failure"), - error.CRL_SIGNATURE_FAILURE => bun.String.static("CRL signature failure"), - error.CERT_NOT_YET_VALID => bun.String.static("certificate is not yet valid"), - error.CRL_NOT_YET_VALID => bun.String.static("CRL is not yet valid"), - error.CERT_HAS_EXPIRED => bun.String.static("certificate has expired"), - error.CRL_HAS_EXPIRED => bun.String.static("CRL has expired"), - error.ERROR_IN_CERT_NOT_BEFORE_FIELD => bun.String.static("format error in certificate's notBefore field"), - error.ERROR_IN_CERT_NOT_AFTER_FIELD => bun.String.static("format error in certificate's notAfter field"), - error.ERROR_IN_CRL_LAST_UPDATE_FIELD => bun.String.static("format error in CRL's lastUpdate field"), - error.ERROR_IN_CRL_NEXT_UPDATE_FIELD => bun.String.static("format error in CRL's nextUpdate field"), - error.OUT_OF_MEM => bun.String.static("out of memory"), - error.DEPTH_ZERO_SELF_SIGNED_CERT => bun.String.static("self signed certificate"), - error.SELF_SIGNED_CERT_IN_CHAIN => bun.String.static("self signed certificate in certificate chain"), - error.UNABLE_TO_GET_ISSUER_CERT_LOCALLY => bun.String.static("unable to get local issuer certificate"), - error.UNABLE_TO_VERIFY_LEAF_SIGNATURE => bun.String.static("unable to verify the first certificate"), - error.CERT_CHAIN_TOO_LONG => bun.String.static("certificate chain too long"), - error.CERT_REVOKED => bun.String.static("certificate revoked"), - error.INVALID_CA => bun.String.static("invalid CA certificate"), - error.INVALID_NON_CA => bun.String.static("invalid non-CA certificate (has CA markings)"), - error.PATH_LENGTH_EXCEEDED => bun.String.static("path length constraint exceeded"), - error.PROXY_PATH_LENGTH_EXCEEDED => bun.String.static("proxy path length constraint exceeded"), - error.PROXY_CERTIFICATES_NOT_ALLOWED => bun.String.static("proxy certificates not allowed, please set the appropriate flag"), - error.INVALID_PURPOSE => bun.String.static("unsupported certificate purpose"), - error.CERT_UNTRUSTED => bun.String.static("certificate not trusted"), - error.CERT_REJECTED => bun.String.static("certificate rejected"), - error.APPLICATION_VERIFICATION => bun.String.static("application verification failure"), - error.SUBJECT_ISSUER_MISMATCH => bun.String.static("subject issuer mismatch"), - error.AKID_SKID_MISMATCH => bun.String.static("authority and subject key identifier mismatch"), - error.AKID_ISSUER_SERIAL_MISMATCH => bun.String.static("authority and issuer serial number mismatch"), - error.KEYUSAGE_NO_CERTSIGN => bun.String.static("key usage does not include certificate signing"), - error.UNABLE_TO_GET_CRL_ISSUER => bun.String.static("unable to get CRL issuer certificate"), - error.UNHANDLED_CRITICAL_EXTENSION => bun.String.static("unhandled critical extension"), - error.KEYUSAGE_NO_CRL_SIGN => bun.String.static("key usage does not include CRL signing"), - error.KEYUSAGE_NO_DIGITAL_SIGNATURE => bun.String.static("key usage does not include digital signature"), - error.UNHANDLED_CRITICAL_CRL_EXTENSION => bun.String.static("unhandled critical CRL extension"), - error.INVALID_EXTENSION => bun.String.static("invalid or inconsistent certificate extension"), - error.INVALID_POLICY_EXTENSION => bun.String.static("invalid or inconsistent certificate policy extension"), - error.NO_EXPLICIT_POLICY => bun.String.static("no explicit policy"), - error.DIFFERENT_CRL_SCOPE => bun.String.static("Different CRL scope"), - error.UNSUPPORTED_EXTENSION_FEATURE => bun.String.static("Unsupported extension feature"), - error.UNNESTED_RESOURCE => bun.String.static("RFC 3779 resource not subset of parent's resources"), - error.PERMITTED_VIOLATION => bun.String.static("permitted subtree violation"), - error.EXCLUDED_VIOLATION => bun.String.static("excluded subtree violation"), - error.SUBTREE_MINMAX => bun.String.static("name constraints minimum and maximum not supported"), - error.UNSUPPORTED_CONSTRAINT_TYPE => bun.String.static("unsupported name constraint type"), - error.UNSUPPORTED_CONSTRAINT_SYNTAX => bun.String.static("unsupported or invalid name constraint syntax"), - error.UNSUPPORTED_NAME_SYNTAX => bun.String.static("unsupported or invalid name syntax"), - error.CRL_PATH_VALIDATION_ERROR => bun.String.static("CRL path validation error"), - error.SUITE_B_INVALID_VERSION => bun.String.static("Suite B: certificate version invalid"), - error.SUITE_B_INVALID_ALGORITHM => bun.String.static("Suite B: invalid public key algorithm"), - error.SUITE_B_INVALID_CURVE => bun.String.static("Suite B: invalid ECC curve"), - error.SUITE_B_INVALID_SIGNATURE_ALGORITHM => bun.String.static("Suite B: invalid signature algorithm"), - error.SUITE_B_LOS_NOT_ALLOWED => bun.String.static("Suite B: curve not allowed for this LOS"), - error.SUITE_B_CANNOT_SIGN_P_384_WITH_P_256 => bun.String.static("Suite B: cannot sign P-384 with P-256"), - error.HOSTNAME_MISMATCH => bun.String.static("Hostname mismatch"), - error.EMAIL_MISMATCH => bun.String.static("Email address mismatch"), - error.IP_ADDRESS_MISMATCH => bun.String.static("IP address mismatch"), - error.INVALID_CALL => bun.String.static("Invalid certificate verification context"), - error.STORE_LOOKUP => bun.String.static("Issuer certificate lookup error"), - error.NAME_CONSTRAINTS_WITHOUT_SANS => bun.String.static("Issuer has name constraints but leaf has no SANs"), - error.UNKKNOW_CERTIFICATE_VERIFICATION_ERROR => bun.String.static("unknown certificate verification error"), - - else => |e| bun.String.createFormat("{s} fetching \"{}\". For more information, pass `verbose: true` in the second argument to fetch()", .{ - @errorName(e), - path, - }) catch bun.outOfMemory(), - }, - .path = path, - }; - - return .{ .SystemError = fetch_error }; - } - - pub fn onReadableStreamAvailable(ctx: *anyopaque, globalThis: *JSC.JSGlobalObject, readable: JSC.WebCore.ReadableStream) void { - const this = bun.cast(*FetchTasklet, ctx); - this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis); - } - - 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), - .unknown => 0, - }; - } - - fn toBodyValue(this: *FetchTasklet) Body.Value { - if (this.getAbortError()) |err| { - return .{ .Error = err }; - } - 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) Response { - log("toResponse", .{}); - bun.assert(this.metadata != null); - // at this point we always should have metadata - const metadata = this.metadata.?; - const http_response = metadata.response; - this.is_waiting_body = this.result.has_more; - return Response{ - .url = bun.String.createAtomIfPossible(metadata.url), - .redirected = this.result.redirected, - .init = .{ - .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), - .status_code = @as(u16, @truncate(http_response.status_code)), - .status_text = bun.String.createAtomIfPossible(http_response.status), - }, - .body = .{ - .value = this.toBodyValue(), - }, - }; - } - - fn ignoreRemainingResponseBody(this: *FetchTasklet) void { - log("ignoreRemainingResponseBody", .{}); - // enabling streaming will make the http thread to drain into the main thread (aka stop buffering) - // without a stream ref, response body or response instance alive it will just ignore the result - if (this.http) |http_| { - http_.enableBodyStreaming(); - } - // we should not keep the process alive if we are ignoring the body - const vm = this.javascript_vm; - this.poll_ref.unref(vm); - // clean any remaining refereces - this.readable_stream_ref.deinit(); - this.response.deinit(); - - if (this.native_response) |response| { - response.unref(); - this.native_response = null; - } - - this.ignore_data = true; - } - - export fn Bun__FetchResponse_finalize(this: *FetchTasklet) callconv(.C) void { - log("onResponseFinalize", .{}); - if (this.native_response) |response| { - const body = response.body; - // Three scenarios: - // - // 1. We are streaming, in which case we should not ignore the body. - // 2. We were buffering, in which case - // 2a. if we have no promise, we should ignore the body. - // 2b. if we have a promise, we should keep loading the body. - // 3. We never started buffering, in which case we should ignore the body. - // - // Note: We cannot call .get() on the ReadableStreamRef. This is called inside a finalizer. - if (body.value != .Locked or this.readable_stream_ref.held.has()) { - // Scenario 1 or 3. - return; - } - - if (body.value.Locked.promise) |promise| { - if (promise.isEmptyOrUndefinedOrNull()) { - // Scenario 2b. - this.ignoreRemainingResponseBody(); - } - } else { - // Scenario 3. - this.ignoreRemainingResponseBody(); - } - } - } - comptime { - _ = Bun__FetchResponse_finalize; - } - - pub fn onResolve(this: *FetchTasklet) JSValue { - log("onResolve", .{}); - const response = bun.new(Response, this.toResponse()); - const response_js = Response.makeMaybePooled(@as(js.JSContextRef, this.global_this), response); - response_js.ensureStillAlive(); - this.response = JSC.Weak(FetchTasklet).create(response_js, this.global_this, .FetchResponse, this); - this.native_response = response.ref(); - return response_js; - } - - pub fn get( - allocator: std.mem.Allocator, - globalThis: *JSC.JSGlobalObject, - fetch_options: FetchOptions, - promise: JSC.JSPromise.Strong, - ) !*FetchTasklet { - var jsc_vm = globalThis.bunVM(); - var fetch_tasklet = try allocator.create(FetchTasklet); - - fetch_tasklet.* = .{ - .mutex = .{}, - .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(http.AsyncHTTP), - .javascript_vm = jsc_vm, - .request_body = fetch_options.body, - .global_this = globalThis, - .promise = promise, - .request_headers = fetch_options.headers, - .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.transpiler.env.getHttpProxyFor(fetch_options.url); - } - } else { - proxy = jsc_vm.transpiler.env.getHttpProxyFor(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; - } - - // This task gets queued on the HTTP thread. - fetch_tasklet.http.?.* = http.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(), - http.HTTPClientResult.Callback.New( - *FetchTasklet, - // handles response events (on headers, on body, etc.) - FetchTasklet.callback, - ).init(fetch_tasklet), - fetch_options.redirect_type, - .{ - .http_proxy = proxy, - .hostname = fetch_options.hostname, - .signals = fetch_tasklet.signals, - .unix_socket_path = fetch_options.unix_socket_path, - .disable_timeout = fetch_options.disable_timeout, - .disable_keepalive = fetch_options.disable_keepalive, - .disable_decompression = fetch_options.disable_decompression, - .reject_unauthorized = fetch_options.reject_unauthorized, - .verbose = fetch_options.verbose, - .tls_props = fetch_options.ssl_config, - }, - ); - // enable streaming the write side - const isStream = fetch_tasklet.request_body == .ReadableStream; - fetch_tasklet.http.?.client.flags.is_streaming_request_body = isStream; - fetch_tasklet.is_waiting_request_stream_start = isStream; - if (isStream) { - fetch_tasklet.http.?.request_body = .{ - .stream = .{ - .buffer = .{}, - .ended = false, - }, - }; - } - // TODO is this necessary? the http client already sets the redirect type, - // so manually setting it here seems redundant - if (fetch_options.redirect_type != FetchRedirect.follow) { - fetch_tasklet.http.?.client.remaining_redirect_count = 0; - } - - // we want to return after headers are received - fetch_tasklet.signal_store.header_progress.store(true, .monotonic); - - if (fetch_tasklet.request_body == .Sendfile) { - bun.assert(fetch_options.url.isHTTP()); - bun.assert(fetch_options.proxy == null); - fetch_tasklet.http.?.request_body = .{ .sendfile = fetch_tasklet.request_body.Sendfile }; - } - - if (fetch_tasklet.signal) |signal| { - signal.pendingActivityRef(); - 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.set(this.global_this, reason); - this.abortTask(); - if (this.sink) |wrapper| { - wrapper.sink.abort(); - return; - } - } - - pub fn sendRequestData(this: *FetchTasklet, data: []const u8, ended: bool) void { - if (this.http) |http_| { - http.http_thread.scheduleRequestWrite(http_, data, ended); - } else if (data.len != 3) { - bun.default_allocator.free(data); - } - } - - pub fn abortTask(this: *FetchTasklet) void { - this.signal_store.aborted.store(true, .monotonic); - this.tracker.didCancel(this.global_this); - - if (this.http) |http_| { - http.http_thread.scheduleShutdown(http_); - } - } - - const FetchOptions = struct { - method: Method, - headers: Headers, - body: HTTPRequestBody, - disable_timeout: bool, - disable_keepalive: bool, - disable_decompression: bool, - reject_unauthorized: bool, - url: ZigURL, - verbose: http.HTTPVerboseLevel = .none, - 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 = .empty, - unix_socket_path: ZigString.Slice, - ssl_config: ?*SSLConfig = null, - }; - - pub fn queue( - allocator: std.mem.Allocator, - global: *JSGlobalObject, - fetch_options: FetchOptions, - promise: JSC.JSPromise.Strong, - ) !*FetchTasklet { - http.HTTPThread.init(&.{}); - var node = try get( - allocator, - global, - fetch_options, - promise, - ); - - var batch = bun.ThreadPool.Batch{}; - node.http.?.schedule(allocator, &batch); - node.poll_ref.ref(global.bunVM()); - - // increment ref so we can keep it alive until the http client is done - node.ref(); - http.http_thread.schedule(batch); - - return node; - } - - /// Called from HTTP thread. Handles HTTP events received from socket. - pub fn callback(task: *FetchTasklet, async_http: *http.AsyncHTTP, result: http.HTTPClientResult) void { - // at this point only this thread is accessing result to is no race condition - const is_done = !result.has_more; - // we are done with the http client so we can deref our side - // this is a atomic operation and will enqueue a task to deinit on the main thread - defer if (is_done) task.derefFromThread(); - - task.mutex.lock(); - // we need to unlock before task.deref(); - defer task.mutex.unlock(); - task.http.?.* = async_http.*; - task.http.?.response_buffer = async_http.response_buffer; - - log("callback success={} has_more={} bytes={}", .{ result.isSuccess(), result.has_more, result.body.?.list.items.len }); - - const prev_metadata = task.result.metadata; - const prev_cert_info = task.result.certificate_info; - task.result = result; - - // Preserve pending certificate info if it was preovided in the previous update. - if (task.result.certificate_info == null) { - if (prev_cert_info) |cert_info| { - task.result.certificate_info = cert_info; - } - } - - // metadata should be provided only once - if (result.metadata orelse prev_metadata) |metadata| { - log("added callback metadata", .{}); - if (task.metadata == null) { - task.metadata = metadata; - } - - task.result.metadata = null; - } - - task.body_size = result.body_size; - - const success = result.isSuccess(); - task.response_buffer = result.body.?.*; - - if (task.ignore_data) { - task.response_buffer.reset(); - - if (task.scheduled_response_buffer.list.capacity > 0) { - task.scheduled_response_buffer.deinit(); - task.scheduled_response_buffer = .{ - .allocator = task.memory_reporter.allocator(), - .list = .{ - .items = &.{}, - .capacity = 0, - }, - }; - } - if (success and result.has_more) { - // we are ignoring the body so we should not receive more data, so will only signal when result.has_more = true - return; - } - } else { - if (success) { - _ = task.scheduled_response_buffer.write(task.response_buffer.list.items) catch bun.outOfMemory(); - } - // reset for reuse - task.response_buffer.reset(); - } - - if (task.has_schedule_callback.cmpxchgStrong(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 = bun.new( - Response, - Response{ - .body = Body{ - .value = .{ - .Blob = blob, - }, - }, - .init = Response.Init{ - .status_code = 200, - .status_text = bun.String.createAtomASCII("OK"), - }, - .url = data_url.url.dupeRef(), - }, - ); - - return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); - } - - comptime { - const Bun__fetchPreconnect = JSC.toJSHostFunction(Bun__fetchPreconnect_); - @export(&Bun__fetchPreconnect, .{ .name = "Bun__fetchPreconnect" }); - } - pub fn Bun__fetchPreconnect_( - globalObject: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(1).slice(); - - if (arguments.len < 1) { - return globalObject.throwNotEnoughArguments("fetch.preconnect", 1, arguments.len); - } - - var url_str = try JSC.URL.hrefFromJS(arguments[0], globalObject); - defer url_str.deref(); - - if (globalObject.hasException()) { - return .zero; - } - - if (url_str.tag == .Dead) { - return globalObject.ERR_INVALID_ARG_TYPE("Invalid URL", .{}).throw(); - } - - if (url_str.isEmpty()) { - return globalObject.ERR_INVALID_ARG_TYPE(fetch_error_blank_url, .{}).throw(); - } - - const url = ZigURL.parse(url_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory()); - if (!url.isHTTP() and !url.isHTTPS() and !url.isS3()) { - bun.default_allocator.free(url.href); - return globalObject.throwInvalidArguments("URL must be HTTP or HTTPS", .{}); - } - - if (url.hostname.len == 0) { - bun.default_allocator.free(url.href); - return globalObject.ERR_INVALID_ARG_TYPE(fetch_error_blank_url, .{}).throw(); - } - - if (!url.hasValidPort()) { - bun.default_allocator.free(url.href); - return globalObject.throwInvalidArguments("Invalid port", .{}); - } - - bun.http.AsyncHTTP.preconnect(url, true); - return .undefined; - } - - const StringOrURL = struct { - pub fn fromJS(value: JSC.JSValue, globalThis: *JSC.JSGlobalObject) bun.JSError!?bun.String { - if (value.isString()) { - return try bun.String.fromJS(value, globalThis); - } - - const out = try JSC.URL.hrefFromJS(value, globalThis); - if (out.tag == .Dead) return null; - return out; - } - }; - - comptime { - const Bun__fetch = JSC.toJSHostFunction(Bun__fetch_); - @export(&Bun__fetch, .{ .name = "Bun__fetch" }); - } - - /// Implementation of `Bun.fetch` - pub fn Bun__fetch_( - ctx: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - JSC.markBinding(@src()); - const globalThis = ctx; - const arguments = callframe.arguments_old(2); - bun.Analytics.Features.fetch += 1; - const vm = JSC.VirtualMachine.get(); - - var memory_reporter = bun.default_allocator.create(JSC.MemoryReportingAllocator) catch bun.outOfMemory(); - // used to clean up dynamically allocated memory on error (a poor man's errdefer) - var is_error = false; - var allocator = memory_reporter.wrap(bun.default_allocator); - errdefer bun.default_allocator.destroy(memory_reporter); - defer { - memory_reporter.report(globalThis.vm()); - - if (is_error) 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 args = JSC.Node.ArgumentsSlice.init(vm, arguments.slice()); - - 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: FetchTasklet.HTTPRequestBody = FetchTasklet.HTTPRequestBody.Empty; - - var disable_timeout = false; - var disable_keepalive = false; - var disable_decompression = false; - var verbose: http.HTTPVerboseLevel = if (vm.log.level.atLeast(.debug)) .headers else .none; - if (verbose == .none) { - verbose = vm.getVerboseFetch(); - } - - var proxy: ?ZigURL = null; - var redirect_type: FetchRedirect = FetchRedirect.follow; - var signal: ?*JSC.WebCore.AbortSignal = null; - // Custom Hostname - var hostname: ?[]u8 = null; - var range: ?[]u8 = null; - var unix_socket_path: ZigString.Slice = ZigString.Slice.empty; - - var url_proxy_buffer: []const u8 = ""; - const URLType = enum { - remote, - file, - blob, - }; - var url_type = URLType.remote; - - var ssl_config: ?*SSLConfig = null; - var reject_unauthorized = vm.getTLSRejectUnauthorized(); - var check_server_identity: JSValue = .zero; - - defer { - if (signal) |sig| { - signal = null; - sig.unref(); - } - - unix_socket_path.deinit(); - - allocator.free(url_proxy_buffer); - url_proxy_buffer = ""; - - if (headers) |*headers_| { - headers_.buf.deinit(allocator); - headers_.entries.deinit(allocator); - headers = null; - } - - body.detach(); - - // clean hostname if any - if (hostname) |hn| { - bun.default_allocator.free(hn); - hostname = null; - } - if (range) |range_| { - bun.default_allocator.free(range_); - range = null; - } - - if (ssl_config) |conf| { - ssl_config = null; - conf.deinit(); - bun.default_allocator.destroy(conf); - } - } - - const options_object: ?JSValue = brk: { - if (args.nextEat()) |options| { - if (options.isObject() or options.jsType() == .DOMWrapper) { - break :brk options; - } - } - - break :brk null; - }; - const request: ?*Request = brk: { - if (first_arg.isCell()) { - if (first_arg.asDirect(Request)) |request_| { - break :brk request_; - } - } - - break :brk null; - }; - // If it's NOT a Request or a subclass of Request, treat the first argument as a URL. - const url_str_optional = if (first_arg.as(Request) == null) try StringOrURL.fromJS(first_arg, globalThis) else null; - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - const request_init_object: ?JSValue = brk: { - if (request != null) break :brk null; - if (url_str_optional != null) break :brk null; - if (first_arg.isObject()) break :brk first_arg; - break :brk null; - }; - - var url_str = extract_url: { - if (url_str_optional) |str| break :extract_url str; - - if (request) |req| { - req.ensureURL() catch bun.outOfMemory(); - break :extract_url req.url.dupeRef(); - } - - if (request_init_object) |request_init| { - if (request_init.fastGet(globalThis, .url)) |url_| { - if (!url_.isUndefined()) { - break :extract_url try bun.String.fromJS(url_, globalThis); - } - } - } - - break :extract_url bun.String.empty; - }; - defer url_str.deref(); - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - if (url_str.isEmpty()) { - is_error = true; - const err = JSC.toTypeError(.ERR_INVALID_URL, fetch_error_blank_url, .{}, ctx); - return JSPromise.rejectedPromiseValue(globalThis, err); - } - - if (url_str.hasPrefixComptime("data:")) { - var url_slice = url_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", .{}); - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, err); - }; - - data_url.url = url_str; - return dataURLResponse(data_url, globalThis, allocator); - } - - url = ZigURL.fromString(allocator, url_str) catch { - const err = JSC.toTypeError(.ERR_INVALID_URL, "fetch() URL is invalid", .{}, ctx); - is_error = true; - return JSPromise.rejectedPromiseValue( - globalThis, - err, - ); - }; - if (url.isFile()) { - url_type = URLType.file; - } else if (url.isBlob()) { - url_type = URLType.blob; - } - url_proxy_buffer = url.href; - - if (url_str.hasPrefixComptime("data:")) { - var url_slice = url_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 = url_str; - - return dataURLResponse(data_url, globalThis, allocator); - } - - // **Start with the harmless ones.** - - // "method" - method = extract_method: { - if (options_object) |options| { - if (try options.getTruthyComptime(globalThis, "method")) |method_| { - break :extract_method try Method.fromJS(globalThis, method_); - } - } - - if (request) |req| { - break :extract_method req.method; - } - - if (request_init_object) |req| { - if (try req.getTruthyComptime(globalThis, "method")) |method_| { - break :extract_method try Method.fromJS(globalThis, method_); - } - } - - break :extract_method null; - } orelse .GET; - - // "decompress: boolean" - disable_decompression = extract_disable_decompression: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "decompress")) |decompression_value| { - if (decompression_value.isBoolean()) { - break :extract_disable_decompression !decompression_value.asBoolean(); - } else if (decompression_value.isNumber()) { - break :extract_disable_decompression decompression_value.to(i32) == 0; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - - break :extract_disable_decompression disable_decompression; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // "tls: TLSConfig" - ssl_config = extract_ssl_config: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "tls")) |tls| { - if (tls.isObject()) { - if (try tls.get(ctx, "rejectUnauthorized")) |reject| { - if (reject.isBoolean()) { - reject_unauthorized = reject.asBoolean(); - } else if (reject.isNumber()) { - reject_unauthorized = reject.to(i32) != 0; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - if (try tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| { - if (checkServerIdentity.isCell() and checkServerIdentity.isCallable()) { - check_server_identity = checkServerIdentity; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - if (SSLConfig.fromJS(vm, globalThis, tls) catch { - is_error = true; - return .zero; - }) |config| { - const ssl_config_object = bun.default_allocator.create(SSLConfig) catch bun.outOfMemory(); - ssl_config_object.* = config; - break :extract_ssl_config ssl_config_object; - } - } - } - } - } - - break :extract_ssl_config ssl_config; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // unix: string | undefined - unix_socket_path = extract_unix_socket_path: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "unix")) |socket_path| { - if (socket_path.isString() and socket_path.getLength(ctx) > 0) { - if (socket_path.toSliceCloneWithAllocator(globalThis, allocator)) |slice| { - break :extract_unix_socket_path slice; - } - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - break :extract_unix_socket_path unix_socket_path; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // timeout: false | number | undefined - disable_timeout = extract_disable_timeout: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "timeout")) |timeout_value| { - if (timeout_value.isBoolean()) { - break :extract_disable_timeout !timeout_value.asBoolean(); - } else if (timeout_value.isNumber()) { - break :extract_disable_timeout timeout_value.to(i32) == 0; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - - break :extract_disable_timeout disable_timeout; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // redirect: "follow" | "error" | "manual" | undefined; - redirect_type = extract_redirect_type: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (objects_to_try[i].getOptionalEnum(globalThis, "redirect", FetchRedirect) catch { - is_error = true; - return .zero; - }) |redirect_value| { - break :extract_redirect_type redirect_value; - } - } - } - - break :extract_redirect_type redirect_type; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // keepalive: boolean | undefined; - disable_keepalive = extract_disable_keepalive: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "keepalive")) |keepalive_value| { - if (keepalive_value.isBoolean()) { - break :extract_disable_keepalive !keepalive_value.asBoolean(); - } else if (keepalive_value.isNumber()) { - break :extract_disable_keepalive keepalive_value.to(i32) == 0; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - - break :extract_disable_keepalive disable_keepalive; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // verbose: boolean | "curl" | undefined; - verbose = extract_verbose: { - const objects_to_try = [_]JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "verbose")) |verb| { - if (verb.isString()) { - if ((try verb.getZigString(globalThis)).eqlComptime("curl")) { - break :extract_verbose .curl; - } - } else if (verb.isBoolean()) { - break :extract_verbose if (verb.toBoolean()) .headers else .none; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - break :extract_verbose verbose; - }; - - // proxy: string | undefined; - url_proxy_buffer = extract_proxy: { - const objects_to_try = [_]JSC.JSValue{ - options_object orelse .zero, - request_init_object orelse .zero, - }; - inline for (0..2) |i| { - if (objects_to_try[i] != .zero) { - if (try objects_to_try[i].get(globalThis, "proxy")) |proxy_arg| { - if (proxy_arg.isString() and proxy_arg.getLength(ctx) > 0) { - var href = try JSC.URL.hrefFromJS(proxy_arg, globalThis); - if (href.tag == .Dead) { - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx); - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, err); - } - defer href.deref(); - const buffer = try std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }); - url = ZigURL.parse(buffer[0..url.href.len]); - if (url.isFile()) { - url_type = URLType.file; - } else if (url.isBlob()) { - url_type = URLType.blob; - } - - proxy = ZigURL.parse(buffer[url.href.len..]); - allocator.free(url_proxy_buffer); - break :extract_proxy buffer; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - } - - break :extract_proxy url_proxy_buffer; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // signal: AbortSignal | undefined; - signal = extract_signal: { - if (options_object) |options| { - if (try options.get(globalThis, "signal")) |signal_| { - if (!signal_.isUndefined()) { - if (signal_.as(JSC.WebCore.AbortSignal)) |signal__| { - break :extract_signal signal__.ref(); - } - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - - if (request) |req| { - if (req.signal) |signal_| { - break :extract_signal signal_.ref(); - } - break :extract_signal null; - } - - if (request_init_object) |options| { - if (try options.get(globalThis, "signal")) |signal_| { - if (signal_.isUndefined()) { - break :extract_signal null; - } - - if (signal_.as(JSC.WebCore.AbortSignal)) |signal__| { - break :extract_signal signal__.ref(); - } - } - } - - break :extract_signal null; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // We do this 2nd to last instead of last so that if it's a FormData - // object, we can still insert the boundary. - // - // body: BodyInit | null | undefined; - // - body = extract_body: { - if (options_object) |options| { - if (options.fastGet(globalThis, .body)) |body__| { - if (!body__.isUndefined()) { - break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - - if (request) |req| { - if (req.body.value == .Used or (req.body.value == .Locked and (req.body.value.Locked.action != .none or req.body.value.Locked.isDisturbed(Request, globalThis, first_arg)))) { - return globalThis.ERR_BODY_ALREADY_USED("Request body already used", .{}).throw(); - } - - if (req.body.value == .Locked) { - if (req.body.value.Locked.readable.has()) { - break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get(globalThis).?, globalThis) }; - } - const readable = req.body.value.toReadableStream(globalThis); - if (!readable.isEmptyOrUndefinedOrNull() and req.body.value == .Locked and req.body.value.Locked.readable.has()) { - break :extract_body FetchTasklet.HTTPRequestBody{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(req.body.value.Locked.readable.get(globalThis).?, globalThis) }; - } - } - - break :extract_body FetchTasklet.HTTPRequestBody{ .AnyBlob = req.body.value.useAsAnyBlob() }; - } - - if (request_init_object) |req| { - if (req.fastGet(globalThis, .body)) |body__| { - if (!body__.isUndefined()) { - break :extract_body try FetchTasklet.HTTPRequestBody.fromJS(ctx, body__); - } - } - } - - break :extract_body null; - } orelse FetchTasklet.HTTPRequestBody.Empty; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // headers: Headers | undefined; - headers = extract_headers: { - var fetch_headers_to_deref: ?*JSC.FetchHeaders = null; - defer { - if (fetch_headers_to_deref) |fetch_headers| { - fetch_headers.deref(); - } - } - - const fetch_headers: ?*JSC.FetchHeaders = brk: { - if (options_object) |options| { - if (options.fastGet(globalThis, .headers)) |headers_value| { - if (!headers_value.isUndefined()) { - if (headers_value.as(FetchHeaders)) |headers__| { - if (headers__.isEmpty()) { - break :brk null; - } - - break :brk headers__; - } - - if (FetchHeaders.createFromJS(ctx, headers_value)) |headers__| { - fetch_headers_to_deref = headers__; - break :brk headers__; - } - - break :brk null; - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - } - - if (request) |req| { - if (req.getFetchHeadersUnlessEmpty()) |head| { - break :brk head; - } - - break :brk null; - } - - if (request_init_object) |options| { - if (options.fastGet(globalThis, .headers)) |headers_value| { - if (!headers_value.isUndefined()) { - if (headers_value.as(FetchHeaders)) |headers__| { - if (headers__.isEmpty()) { - break :brk null; - } - - break :brk headers__; - } - - if (FetchHeaders.createFromJS(ctx, headers_value)) |headers__| { - fetch_headers_to_deref = headers__; - break :brk headers__; - } - - break :brk null; - } - } - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - break :extract_headers headers; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - if (fetch_headers) |headers_| { - if (headers_.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { - if (hostname) |host| { - hostname = null; - allocator.free(host); - } - hostname = _hostname.toOwnedSliceZ(allocator) catch bun.outOfMemory(); - } - if (url.isS3()) { - if (headers_.fastGet(JSC.FetchHeaders.HTTPHeaderName.Range)) |_range| { - if (range) |range_| { - range = null; - allocator.free(range_); - } - range = _range.toOwnedSliceZ(allocator) catch bun.outOfMemory(); - } - } - - break :extract_headers Headers.from(headers_, allocator, .{ .body = body.getAnyBlob() }) catch bun.outOfMemory(); - } - - break :extract_headers headers; - }; - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - if (proxy != null and unix_socket_path.length() > 0) { - is_error = true; - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_proxy_unix, .{}, ctx); - return JSPromise.rejectedPromiseValue(globalThis, err); - } - - if (globalThis.hasException()) { - is_error = true; - return .zero; - } - - // 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 (url_type != .remote) { - defer unix_socket_path.deinit(); - var path_buf: bun.PathBuffer = undefined; - const PercentEncoding = @import("../../url.zig").PercentEncoding; - var path_buf2: bun.PathBuffer = undefined; - var stream = std.io.fixedBufferStream(&path_buf2); - var url_path_decoded = path_buf2[0 .. PercentEncoding.decode( - @TypeOf(&stream.writer()), - &stream.writer(), - switch (url_type) { - .file => url.path, - .blob => url.href["blob:".len..], - .remote => unreachable, - }, - ) catch |err| { - return globalThis.throwError(err, "Failed to decode file url"); - }]; - var url_string: bun.String = bun.String.empty; - defer url_string.deref(); - // This can be a blob: url or a file: url. - const blob_to_use = blob: { - - // Support blob: urls - if (url_type == URLType.blob) { - if (JSC.WebCore.ObjectURLRegistry.singleton().resolveAndDupe(url_path_decoded)) |blob| { - url_string = bun.String.createFormat("blob:{s}", .{url_path_decoded}) catch bun.outOfMemory(); - break :blob blob; - } else { - // Consistent with what Node.js does - it rejects, not a 404. - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "Failed to resolve blob:{s}", .{ - url_path_decoded, - }, ctx); - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, err); - } - } - - const temp_file_path = brk: { - if (std.fs.path.isAbsolute(url_path_decoded)) { - if (Environment.isWindows) { - // pathname will start with / if is a absolute path on windows, so we remove before normalizing it - if (url_path_decoded[0] == '/') { - url_path_decoded = url_path_decoded[1..]; - } - break :brk PosixToWinNormalizer.resolveCWDWithExternalBufZ(&path_buf, url_path_decoded) catch |err| { - return globalThis.throwError(err, "Failed to resolve file url"); - }; - } - break :brk url_path_decoded; - } - - var cwd_buf: bun.PathBuffer = undefined; - const cwd = if (Environment.isWindows) (bun.getcwd(&cwd_buf) catch |err| { - return globalThis.throwError(err, "Failed to resolve file url"); - }) else globalThis.bunVM().transpiler.fs.top_level_dir; - - const fullpath = bun.path.joinAbsStringBuf( - cwd, - &path_buf, - &[_]string{ - globalThis.bunVM().main, - "../", - url_path_decoded, - }, - .auto, - ); - if (Environment.isWindows) { - break :brk PosixToWinNormalizer.resolveCWDWithExternalBufZ(&path_buf2, fullpath) catch |err| { - return globalThis.throwError(err, "Failed to resolve file url"); - }; - } - break :brk fullpath; - }; - - url_string = JSC.URL.fileURLFromString(bun.String.fromUTF8(temp_file_path)); - - var pathlike: JSC.Node.PathOrFileDescriptor = .{ - .path = .{ - .encoded_slice = ZigString.Slice.init(bun.default_allocator, try bun.default_allocator.dupe(u8, temp_file_path)), - }, - }; - - break :blob Blob.findOrCreateFileFromPath( - &pathlike, - globalThis, - true, - ); - }; - - const response = bun.new(Response, Response{ - .body = Body{ - .value = .{ .Blob = blob_to_use }, - }, - .init = Response.Init{ - .status_code = 200, - }, - .url = url_string.clone(), - }); - - return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis)); - } - - if (url.protocol.len > 0) { - if (!(url.isHTTP() or url.isHTTPS() or url.isS3())) { - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http:, https: or s3:", .{}, ctx); - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, err); - } - } - - if (!method.hasRequestBody() and body.hasBody()) { - const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx); - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, err); - } - - if (headers == null and body.hasBody() and body.hasContentTypeFromUser()) { - headers = Headers.from( - null, - allocator, - .{ .body = body.getAnyBlob() }, - ) catch bun.outOfMemory(); - } - - var http_body = body; - if (body.isS3()) { - prepare_body: { - // is a S3 file we can use chunked here - - if (JSC.WebCore.ReadableStream.fromJS(JSC.WebCore.ReadableStream.fromBlob(globalThis, &body.AnyBlob.Blob, s3.MultiPartUploadOptions.DefaultPartSize), globalThis)) |stream| { - var old = body; - defer old.detach(); - body = .{ .ReadableStream = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis) }; - break :prepare_body; - } - const rejected_value = JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Failed to start s3 stream", .{})); - body.detach(); - - return rejected_value; - } - } - if (body.needsToReadFile()) { - prepare_body: { - const opened_fd_res: JSC.Maybe(bun.FileDescriptor) = switch (body.store().?.data.file.pathlike) { - .fd => |fd| bun.sys.dup(fd), - .path => |path| bun.sys.open(path.sliceZ(&globalThis.bunVM().nodeFS().sync_error_buf), if (Environment.isWindows) bun.O.RDONLY else bun.O.RDONLY | bun.O.NOCTTY, 0), - }; - - const opened_fd = switch (opened_fd_res) { - .err => |err| { - const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); - is_error = 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.AnyBlob.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.AnyBlob.Blob.offset + original_size, - .offset = body.AnyBlob.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.AnyBlob.Blob.offset, - .max_size = body.AnyBlob.Blob.size, - }, - .sync, - ); - - if (body.store().?.data.file.pathlike == .path) { - _ = bun.sys.close(opened_fd); - } - - switch (res) { - .err => |err| { - is_error = true; - const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); - body.detach(); - - return rejected_value; - }, - .result => |result| { - body.detach(); - body = .{ .AnyBlob = .fromOwnedSlice(allocator, @constCast(result.slice())) }; - http_body = .{ .AnyBlob = body.AnyBlob }; - }, - } - } - } - - if (url.isS3()) { - // get ENV config - var credentialsWithOptions: s3.S3CredentialsWithOptions = .{ - .credentials = globalThis.bunVM().transpiler.env.getS3Credentials(), - .options = .{}, - .acl = null, - .storage_class = null, - }; - defer { - credentialsWithOptions.deinit(); - } - - if (options_object) |options| { - if (try options.getTruthyComptime(globalThis, "s3")) |s3_options| { - if (s3_options.isObject()) { - s3_options.ensureStillAlive(); - credentialsWithOptions = try s3.S3Credentials.getCredentialsWithOptions(credentialsWithOptions.credentials, .{}, s3_options, null, null, globalThis); - } - } - } - - if (body == .ReadableStream) { - // we cannot direct stream to s3 we need to use multi part upload - defer body.ReadableStream.deinit(); - const Wrapper = struct { - promise: JSC.JSPromise.Strong, - url: ZigURL, - url_proxy_buffer: []const u8, - global: *JSC.JSGlobalObject, - - pub usingnamespace bun.New(@This()); - - pub fn resolve(result: s3.S3UploadResult, self: *@This()) void { - const global = self.global; - switch (result) { - .success => { - const response = bun.new(Response, Response{ - .body = .{ .value = .Empty }, - .redirected = false, - .init = .{ .method = .PUT, .status_code = 200 }, - .url = bun.String.createAtomIfPossible(self.url.href), - }); - const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); - response_js.ensureStillAlive(); - self.promise.resolve(global, response_js); - }, - .failure => |err| { - const response = bun.new(Response, Response{ - .body = .{ - .value = .{ - .InternalBlob = .{ - .bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, bun.default_allocator.dupe(u8, err.message) catch bun.outOfMemory()), - .was_string = true, - }, - }, - }, - .redirected = false, - .init = .{ - .method = .PUT, - .status_code = 500, - .status_text = bun.String.createAtomIfPossible(err.code), - }, - .url = bun.String.createAtomIfPossible(self.url.href), - }); - const response_js = Response.makeMaybePooled(@as(js.JSContextRef, global), response); - response_js.ensureStillAlive(); - self.promise.resolve(global, response_js); - }, - } - bun.default_allocator.free(self.url_proxy_buffer); - self.destroy(); - } - }; - if (method != .PUT and method != .POST) { - return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("Only POST and PUT do support body when using S3", .{})); - } - const promise = JSC.JSPromise.Strong.init(globalThis); - - const s3_stream = Wrapper.new(.{ - .url = url, - .url_proxy_buffer = url_proxy_buffer, - .promise = promise, - .global = globalThis, - }); - - const promise_value = promise.value(); - const proxy_url = if (proxy) |p| p.href else ""; - _ = bun.S3.uploadStream( - credentialsWithOptions.credentials.dupe(), - url.s3Path(), - body.ReadableStream.get(globalThis).?, - globalThis, - credentialsWithOptions.options, - credentialsWithOptions.acl, - credentialsWithOptions.storage_class, - if (headers) |h| h.getContentType() else null, - proxy_url, - @ptrCast(&Wrapper.resolve), - s3_stream, - ); - url = .{}; - url_proxy_buffer = ""; - return promise_value; - } - if (method == .POST) { - method = .PUT; - } - - var result = credentialsWithOptions.credentials.signRequest(.{ - .path = url.s3Path(), - .method = method, - }, null) catch |sign_err| { - is_error = true; - return JSPromise.rejectedPromiseValue(globalThis, s3.getJSSignError(sign_err, globalThis)); - }; - defer result.deinit(); - if (proxy) |proxy_| { - // proxy and url are in the same buffer lets replace it - const old_buffer = url_proxy_buffer; - defer allocator.free(old_buffer); - var buffer = allocator.alloc(u8, result.url.len + proxy_.href.len) catch bun.outOfMemory(); - bun.copy(u8, buffer[0..result.url.len], result.url); - bun.copy(u8, buffer[proxy_.href.len..], proxy_.href); - url_proxy_buffer = buffer; - - url = ZigURL.parse(url_proxy_buffer[0..result.url.len]); - proxy = ZigURL.parse(url_proxy_buffer[result.url.len..]); - } else { - // replace headers and url of the request - allocator.free(url_proxy_buffer); - url_proxy_buffer = result.url; - url = ZigURL.parse(result.url); - result.url = ""; // fetch now owns this - } - - const content_type = if (headers) |h| h.getContentType() else null; - var header_buffer: [10]picohttp.Header = undefined; - - if (range) |range_| { - const _headers = result.mixWithHeader(&header_buffer, .{ .name = "range", .value = range_ }); - setHeaders(&headers, _headers, allocator); - } else if (content_type) |ct| { - if (ct.len > 0) { - const _headers = result.mixWithHeader(&header_buffer, .{ .name = "Content-Type", .value = ct }); - setHeaders(&headers, _headers, allocator); - } else { - setHeaders(&headers, result.headers(), allocator); - } - } else { - setHeaders(&headers, result.headers(), allocator); - } - } - - // 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(); - - const initial_body_reference_count: if (Environment.isDebug) usize else u0 = brk: { - if (Environment.isDebug) { - if (body.store()) |store| { - break :brk store.ref_count.load(.monotonic); - } - } - - break :brk 0; - }; - - _ = FetchTasklet.queue( - allocator, - globalThis, - .{ - .method = method, - .url = url, - .headers = headers orelse Headers{ - .allocator = allocator, - }, - .body = http_body, - .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, - .ssl_config = ssl_config, - .hostname = hostname, - .memory_reporter = memory_reporter, - .check_server_identity = if (check_server_identity.isEmptyOrUndefinedOrNull()) .empty else .create(check_server_identity, globalThis), - .unix_socket_path = unix_socket_path, - }, - // 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 bun.outOfMemory(); - - if (Environment.isDebug) { - if (body.store()) |store| { - if (store.ref_count.load(.monotonic) == initial_body_reference_count) { - Output.panic("Expected body ref count to have incremented in FetchTasklet", .{}); - } - } - } - - // These are now owned by FetchTasklet. - url = .{}; - headers = null; - // Reference count for the blob is incremented above. - if (body.store() != null) { - body.detach(); - } else { - // These are single-use, and have effectively been moved to the FetchTasklet. - body = FetchTasklet.HTTPRequestBody.Empty; - } - proxy = null; - url_proxy_buffer = ""; - signal = null; - ssl_config = null; - hostname = null; - unix_socket_path = ZigString.Slice.empty; - - return promise_val; - } -}; - /// https://developer.mozilla.org/en-US/docs/Web/API/Headers // TODO: move to http.zig. this has nothing to do with JSC or WebCore pub const Headers = struct { @@ -3676,3 +939,4 @@ pub const Headers = struct { return headers; } }; +pub const Fetch = @import("fetch.zig");