Files
bun.sh/src/bun.js/webcore/response.zig

2232 lines
92 KiB
Zig

const std = @import("std");
const Api = @import("../../api/schema.zig").Api;
const bun = @import("root").bun;
const RequestContext = @import("../../bun_dev_http_server.zig").RequestContext;
const MimeType = @import("../../bun_dev_http_server.zig").MimeType;
const ZigURL = @import("../../url.zig").URL;
const HTTPClient = @import("root").bun.HTTP;
const FetchRedirect = HTTPClient.FetchRedirect;
const NetworkThread = HTTPClient.NetworkThread;
const AsyncIO = NetworkThread.AsyncIO;
const JSC = @import("root").bun.JSC;
const js = JSC.C;
const Method = @import("../../http/method.zig").Method;
const FetchHeaders = JSC.FetchHeaders;
const ObjectPool = @import("../../pool.zig").ObjectPool;
const SystemError = JSC.SystemError;
const Output = @import("root").bun.Output;
const MutableString = @import("root").bun.MutableString;
const strings = @import("root").bun.strings;
const string = @import("root").bun.string;
const default_allocator = @import("root").bun.default_allocator;
const FeatureFlags = @import("root").bun.FeatureFlags;
const ArrayBuffer = @import("../base.zig").ArrayBuffer;
const Properties = @import("../base.zig").Properties;
const castObj = @import("../base.zig").castObj;
const getAllocator = @import("../base.zig").getAllocator;
const GetJSPrivateData = @import("../base.zig").GetJSPrivateData;
const Environment = @import("../../env.zig");
const ZigString = JSC.ZigString;
const IdentityContext = @import("../../identity_context.zig").IdentityContext;
const JSPromise = JSC.JSPromise;
const JSValue = JSC.JSValue;
const JSError = JSC.JSError;
const JSGlobalObject = JSC.JSGlobalObject;
const NullableAllocator = @import("../../nullable_allocator.zig").NullableAllocator;
const DataURL = @import("../../resolver/data_url.zig").DataURL;
const VirtualMachine = JSC.VirtualMachine;
const Task = JSC.Task;
const JSPrinter = bun.js_printer;
const picohttp = @import("root").bun.picohttp;
const StringJoiner = @import("../../string_joiner.zig");
const uws = @import("root").bun.uws;
const Mutex = @import("../../lock.zig").Lock;
const InlineBlob = JSC.WebCore.InlineBlob;
const AnyBlob = JSC.WebCore.AnyBlob;
const InternalBlob = JSC.WebCore.InternalBlob;
const BodyMixin = JSC.WebCore.BodyMixin;
const Body = JSC.WebCore.Body;
const Request = JSC.WebCore.Request;
const Blob = JSC.WebCore.Blob;
const Async = bun.Async;
const BoringSSL = bun.BoringSSL;
const X509 = @import("../api/bun/x509.zig");
const BoringSSL = bun.BoringSSL;
const X509 = @import("../api/bun/x509.zig");
pub const Response = struct {
const ResponseMixin = BodyMixin(@This());
pub usingnamespace JSC.Codegen.JSResponse;
allocator: std.mem.Allocator,
body: Body,
url: bun.String = bun.String.empty,
status_text: bun.String = bun.String.empty,
redirected: bool = false,
// We must report a consistent value for this
reported_estimated_size: ?u63 = null,
pub const getText = ResponseMixin.getText;
pub const getBody = ResponseMixin.getBody;
pub const getBodyUsed = ResponseMixin.getBodyUsed;
pub const getJSON = ResponseMixin.getJSON;
pub const getArrayBuffer = ResponseMixin.getArrayBuffer;
pub const getBlob = ResponseMixin.getBlob;
pub const getFormData = ResponseMixin.getFormData;
pub fn getFormDataEncoding(this: *Response) ?*bun.FormData.AsyncFormData {
var content_type_slice: ZigString.Slice = this.getContentType() orelse return null;
defer content_type_slice.deinit();
const encoding = bun.FormData.Encoding.get(content_type_slice.slice()) orelse return null;
return bun.FormData.AsyncFormData.init(this.allocator, encoding) catch unreachable;
}
pub fn estimatedSize(this: *Response) callconv(.C) usize {
return this.reported_estimated_size orelse brk: {
this.reported_estimated_size = @as(
u63,
@intCast(this.body.value.estimatedSize() + this.url.byteSlice().len + this.status_text.byteSlice().len + @sizeOf(Response)),
);
break :brk this.reported_estimated_size.?;
};
}
pub fn getBodyValue(
this: *Response,
) *Body.Value {
return &this.body.value;
}
pub fn getFetchHeaders(
this: *Response,
) ?*FetchHeaders {
return this.body.init.headers;
}
pub inline fn statusCode(this: *const Response) u16 {
return this.body.init.status_code;
}
pub fn redirectLocation(this: *const Response) ?[]const u8 {
return this.header(.Location);
}
pub fn header(this: *const Response, name: JSC.FetchHeaders.HTTPHeaderName) ?[]const u8 {
return if ((this.body.init.headers orelse return null).fastGet(name)) |str|
str.slice()
else
null;
}
pub const Props = struct {};
pub fn writeFormat(this: *Response, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
const Writer = @TypeOf(writer);
try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())});
{
formatter.indent += 1;
defer formatter.indent -|= 1;
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>ok<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>url<d>:<r> \"", enable_ansi_colors));
try writer.print(comptime Output.prettyFmt("<r><b>{}<r>", enable_ansi_colors), .{this.url});
try writer.writeAll("\"");
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>headers<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>statusText<d>:<r> ", enable_ansi_colors));
try writer.print(comptime Output.prettyFmt("<r>\"<b>{}<r>\"", enable_ansi_colors), .{this.status_text});
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>redirected<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
formatter.resetLine();
try this.body.writeFormat(Formatter, formatter, writer, enable_ansi_colors);
}
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll("}");
formatter.resetLine();
}
pub fn isOK(this: *const Response) bool {
return this.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299);
}
pub fn getURL(
this: *Response,
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/url
return this.url.toJS(globalThis);
}
pub fn getResponseType(
this: *Response,
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
if (this.body.init.status_code < 200) {
return ZigString.init("error").toValue(globalThis);
}
return ZigString.init("default").toValue(globalThis);
}
pub fn getStatusText(
this: *Response,
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
return this.status_text.toJS(globalThis);
}
pub fn getRedirected(
this: *Response,
_: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/redirected
return JSValue.jsBoolean(this.redirected);
}
pub fn getOK(
this: *Response,
_: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
return JSValue.jsBoolean(this.isOK());
}
fn getOrCreateHeaders(this: *Response, globalThis: *JSC.JSGlobalObject) *FetchHeaders {
if (this.body.init.headers == null) {
this.body.init.headers = FetchHeaders.createEmpty();
if (this.body.value == .Blob) {
const content_type = this.body.value.Blob.content_type;
if (content_type.len > 0) {
this.body.init.headers.?.put("content-type", content_type, globalThis);
}
}
}
return this.body.init.headers.?;
}
pub fn getHeaders(
this: *Response,
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
return this.getOrCreateHeaders(globalThis).toJS(globalThis);
}
pub fn doClone(
this: *Response,
globalThis: *JSC.JSGlobalObject,
_: *JSC.CallFrame,
) callconv(.C) JSValue {
var cloned = this.clone(getAllocator(globalThis), globalThis);
return Response.makeMaybePooled(globalThis, cloned);
}
pub fn makeMaybePooled(globalObject: *JSC.JSGlobalObject, ptr: *Response) JSValue {
return ptr.toJS(globalObject);
}
pub fn cloneInto(
this: *Response,
new_response: *Response,
allocator: std.mem.Allocator,
globalThis: *JSGlobalObject,
) void {
new_response.* = Response{
.allocator = allocator,
.body = this.body.clone(globalThis),
.url = this.url.clone(),
.status_text = this.status_text.clone(),
.redirected = this.redirected,
};
}
pub fn clone(this: *Response, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Response {
var new_response = allocator.create(Response) catch unreachable;
this.cloneInto(new_response, allocator, globalThis);
return new_response;
}
pub fn getStatus(
this: *Response,
_: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/status
return JSValue.jsNumber(this.body.init.status_code);
}
pub fn finalize(
this: *Response,
) callconv(.C) void {
this.body.deinit(this.allocator);
var allocator = this.allocator;
this.status_text.deref();
this.url.deref();
allocator.destroy(this);
}
pub fn mimeType(response: *const Response, request_ctx_: ?*const RequestContext) string {
if (comptime Environment.isWindows) unreachable;
return mimeTypeWithDefault(response, MimeType.other, request_ctx_);
}
pub fn mimeTypeWithDefault(response: *const Response, default: MimeType, request_ctx_: ?*const RequestContext) string {
if (comptime Environment.isWindows) unreachable;
if (response.header(.ContentType)) |content_type| {
return content_type;
}
if (request_ctx_) |request_ctx| {
if (request_ctx.url.extname.len > 0) {
return MimeType.byExtension(request_ctx.url.extname).value;
}
}
switch (response.body.value) {
.Blob => |blob| {
if (blob.content_type.len > 0) {
return blob.content_type;
}
// auto-detect HTML if unspecified
if (strings.hasPrefixComptime(response.body.value.slice(), "<!DOCTYPE html>")) {
return MimeType.html.value;
}
return default.value;
},
.WTFStringImpl => |str| {
if (bun.String.init(str).hasPrefixComptime("<!DOCTYPE html>")) {
return MimeType.html.value;
}
return default.value;
},
.InternalBlob => {
// auto-detect HTML if unspecified
if (strings.hasPrefixComptime(response.body.value.slice(), "<!DOCTYPE html>")) {
return MimeType.html.value;
}
return response.body.value.InternalBlob.contentType();
},
.Null, .Used, .Locked, .Empty, .Error => return default.value,
}
}
pub fn getContentType(
this: *Response,
) ?ZigString.Slice {
if (this.body.init.headers) |headers| {
if (headers.fastGet(.ContentType)) |value| {
return value.toSlice(bun.default_allocator);
}
}
if (this.body.value == .Blob) {
if (this.body.value.Blob.content_type.len > 0)
return ZigString.Slice.fromUTF8NeverFree(this.body.value.Blob.content_type);
}
return null;
}
pub fn constructJSON(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) JSValue {
const args_list = callframe.arguments(2);
// https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]);
// var response = getAllocator(globalThis).create(Response) catch unreachable;
var response = Response{
.body = Body{
.init = Body.Init{
.status_code = 200,
},
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
.url = bun.String.empty,
};
const json_value = args.nextEat() orelse JSC.JSValue.zero;
if (@intFromEnum(json_value) != 0) {
var str = bun.String.empty;
// calling JSON.stringify on an empty string adds extra quotes
// so this is correct
json_value.jsonStringify(globalThis, 0, &str);
if (!str.isEmpty()) {
if (str.value.WTFStringImpl.toUTF8IfNeeded(bun.default_allocator)) |bytes| {
defer str.deref();
response.body.value = .{
.InternalBlob = InternalBlob{
.bytes = std.ArrayList(u8).fromOwnedSlice(bun.default_allocator, @constCast(bytes.slice())),
.was_string = true,
},
};
} else {
response.body.value = Body.Value{
.WTFStringImpl = str.value.WTFStringImpl,
};
}
}
}
if (args.nextEat()) |init| {
if (init.isUndefinedOrNull()) {} else if (init.isNumber()) {
response.body.init.status_code = @as(u16, @intCast(@min(@max(0, init.toInt32()), std.math.maxInt(u16))));
} else {
if (Body.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| {
response.body.init = _init;
}
}
}
var headers_ref = response.getOrCreateHeaders(globalThis);
headers_ref.putDefault("content-type", MimeType.json.value, globalThis);
var ptr = response.allocator.create(Response) catch unreachable;
ptr.* = response;
return ptr.toJS(globalThis);
}
pub fn constructRedirect(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) JSValue {
var args_list = callframe.arguments(4);
// https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]);
// var response = getAllocator(globalThis).create(Response) catch unreachable;
var response = Response{
.body = Body{
.init = Body.Init{
.status_code = 302,
},
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
.url = bun.String.empty,
};
const url_string_value = args.nextEat() orelse JSC.JSValue.zero;
var url_string = ZigString.init("");
if (@intFromEnum(url_string_value) != 0) {
url_string = url_string_value.getZigString(globalThis.ptr());
}
var url_string_slice = url_string.toSlice(getAllocator(globalThis));
defer url_string_slice.deinit();
if (args.nextEat()) |init| {
if (init.isUndefinedOrNull()) {} else if (init.isNumber()) {
response.body.init.status_code = @as(u16, @intCast(@min(@max(0, init.toInt32()), std.math.maxInt(u16))));
} else {
if (Body.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| {
response.body.init = _init;
response.body.init.status_code = 302;
}
}
}
response.body.init.headers = response.getOrCreateHeaders(globalThis);
var headers_ref = response.body.init.headers.?;
headers_ref.put("location", url_string_slice.slice(), globalThis);
var ptr = response.allocator.create(Response) catch unreachable;
ptr.* = response;
return ptr.toJS(globalThis);
}
pub fn constructError(
globalThis: *JSC.JSGlobalObject,
_: *JSC.CallFrame,
) callconv(.C) JSValue {
var response = getAllocator(globalThis).create(Response) catch unreachable;
response.* = Response{
.body = Body{
.init = Body.Init{
.status_code = 0,
},
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
};
return response.toJS(globalThis);
}
pub fn constructor(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) ?*Response {
const args_list = brk: {
var args = callframe.arguments(2);
if (args.len > 1 and args.ptr[1].isEmptyOrUndefinedOrNull()) {
args.len = 1;
}
break :brk args;
};
const arguments = args_list.ptr[0..args_list.len];
const body: Body = @as(?Body, brk: {
switch (arguments.len) {
0 => {
break :brk Body.@"200"(globalThis);
},
1 => {
break :brk Body.extract(globalThis, arguments[0]);
},
else => {
if (arguments[1].isObject()) {
break :brk Body.extractWithInit(globalThis, arguments[0], arguments[1]);
}
std.debug.assert(!arguments[1].isEmptyOrUndefinedOrNull());
const err = globalThis.createTypeErrorInstance("Expected options to be one of: null, undefined, or object", .{});
globalThis.throwValue(err);
break :brk null;
},
}
unreachable;
}) orelse return null;
var response = getAllocator(globalThis).create(Response) catch unreachable;
response.* = Response{
.body = body,
.allocator = getAllocator(globalThis),
};
if (response.body.value == .Blob and
response.body.init.headers != null and
response.body.value.Blob.content_type.len > 0 and
!response.body.init.headers.?.fastHas(.ContentType))
{
response.body.init.headers.?.put("content-type", response.body.value.Blob.content_type, globalThis);
}
return response;
}
};
const null_fd = bun.invalid_fd;
pub const Fetch = struct {
const headers_string = "headers";
const method_string = "method";
const JSType = js.JSType;
pub const fetch_error_no_args = "fetch() expects a string but received no arguments.";
pub const fetch_error_blank_url = "fetch() URL must not be a blank string.";
pub const fetch_error_unexpected_body = "fetch() request with GET/HEAD/OPTIONS method cannot have body.";
const JSTypeErrorEnum = std.enums.EnumArray(JSType, string);
pub const fetch_type_error_names: JSTypeErrorEnum = brk: {
var errors = JSTypeErrorEnum.initUndefined();
errors.set(JSType.kJSTypeUndefined, "Undefined");
errors.set(JSType.kJSTypeNull, "Null");
errors.set(JSType.kJSTypeBoolean, "Boolean");
errors.set(JSType.kJSTypeNumber, "Number");
errors.set(JSType.kJSTypeString, "String");
errors.set(JSType.kJSTypeObject, "Object");
errors.set(JSType.kJSTypeSymbol, "Symbol");
break :brk errors;
};
pub const fetch_type_error_string_values = .{
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeUndefined)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNull)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeBoolean)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNumber)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeString)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeObject)}),
std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeSymbol)}),
};
pub const fetch_type_error_strings: JSTypeErrorEnum = brk: {
var errors = JSTypeErrorEnum.initUndefined();
errors.set(
JSType.kJSTypeUndefined,
bun.asByteSlice(fetch_type_error_string_values[0]),
);
errors.set(
JSType.kJSTypeNull,
bun.asByteSlice(fetch_type_error_string_values[1]),
);
errors.set(
JSType.kJSTypeBoolean,
bun.asByteSlice(fetch_type_error_string_values[2]),
);
errors.set(
JSType.kJSTypeNumber,
bun.asByteSlice(fetch_type_error_string_values[3]),
);
errors.set(
JSType.kJSTypeString,
bun.asByteSlice(fetch_type_error_string_values[4]),
);
errors.set(
JSType.kJSTypeObject,
bun.asByteSlice(fetch_type_error_string_values[5]),
);
errors.set(
JSType.kJSTypeSymbol,
bun.asByteSlice(fetch_type_error_string_values[6]),
);
break :brk errors;
};
comptime {
if (!JSC.is_bindgen) {
_ = Bun__fetch;
}
}
pub const FetchTasklet = struct {
const log = Output.scoped(.FetchTasklet, false);
http: ?*HTTPClient.AsyncHTTP = null,
result: HTTPClient.HTTPClientResult = .{},
metadata: ?HTTPClient.HTTPResponseMetadata = null,
javascript_vm: *VirtualMachine = undefined,
global_this: *JSGlobalObject = undefined,
request_body: HTTPRequestBody = undefined,
/// buffer being used by AsyncHTTP
response_buffer: MutableString = undefined,
/// buffer used to stream response to JS
scheduled_response_buffer: MutableString = undefined,
/// response strong ref
response: JSC.Strong = .{},
/// stream strong ref if any is available
readable_stream_ref: JSC.WebCore.ReadableStream.Strong = .{},
request_headers: Headers = Headers{ .allocator = undefined },
promise: JSC.JSPromise.Strong,
concurrent_task: JSC.ConcurrentTask = .{},
poll_ref: 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: HTTPClient.HTTPClientResult.BodySize = .unknown,
/// This is url + proxy memory buffer and is owned by FetchTasklet
/// We always clone url and proxy (if informed)
url_proxy_buffer: []const u8 = "",
signal: ?*JSC.WebCore.AbortSignal = null,
signals: HTTPClient.Signals = .{},
signal_store: HTTPClient.Signals.Store = .{},
has_schedule_callback: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false),
// must be stored because AbortSignal stores reason weakly
abort_reason: JSValue = JSValue.zero,
// custom checkServerIdentity
check_server_identity: JSC.Strong = .{},
reject_unauthorized: bool = true,
// Custom Hostname
hostname: ?[]u8 = null,
is_waiting_body: bool = false,
is_waiting_abort: bool = false,
mutex: Mutex,
tracker: JSC.AsyncTaskTracker,
pub const HTTPRequestBody = union(enum) {
AnyBlob: AnyBlob,
Sendfile: HTTPClient.Sendfile,
pub fn store(this: *HTTPRequestBody) ?*JSC.WebCore.Blob.Store {
return switch (this.*) {
.AnyBlob => this.AnyBlob.store(),
else => null,
};
}
pub fn slice(this: *const HTTPRequestBody) []const u8 {
return switch (this.*) {
.AnyBlob => this.AnyBlob.slice(),
else => "",
};
}
pub fn detach(this: *HTTPRequestBody) void {
switch (this.*) {
.AnyBlob => this.AnyBlob.detach(),
.Sendfile => {
if (@max(this.Sendfile.offset, this.Sendfile.remain) > 0)
_ = bun.sys.close(this.Sendfile.fd);
this.Sendfile.offset = 0;
this.Sendfile.remain = 0;
},
}
}
};
pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet {
return FetchTasklet{};
}
fn clearData(this: *FetchTasklet) void {
log("clearData", .{});
const allocator = this.memory_reporter.allocator();
if (this.url_proxy_buffer.len > 0) {
allocator.free(this.url_proxy_buffer);
this.url_proxy_buffer.len = 0;
}
if (this.hostname) |hostname| {
allocator.free(hostname);
this.hostname = null;
}
this.request_headers.entries.deinit(allocator);
this.request_headers.buf.deinit(allocator);
this.request_headers = Headers{ .allocator = undefined };
if (this.http != null) {
this.http.?.clearData();
}
if (this.metadata != null) {
this.metadata.?.deinit(allocator);
this.metadata = null;
}
this.response_buffer.deinit();
this.response.deinit();
this.readable_stream_ref.deinit();
this.scheduled_response_buffer.deinit();
this.request_body.detach();
if (this.abort_reason != .zero)
this.abort_reason.unprotect();
this.check_server_identity.deinit();
if (this.signal) |signal| {
this.signal = null;
signal.detach(this);
}
}
pub fn deinit(this: *FetchTasklet) void {
log("deinit", .{});
var reporter = this.memory_reporter;
const allocator = reporter.allocator();
if (this.http) |http| allocator.destroy(http);
allocator.destroy(this);
// reporter.assert();
bun.default_allocator.destroy(reporter);
}
pub fn onBodyReceived(this: *FetchTasklet) void {
this.mutex.lock();
const success = this.result.isSuccess();
const globalThis = this.global_this;
const is_done = !success or !this.result.has_more;
defer {
this.has_schedule_callback.store(false, .Monotonic);
this.mutex.unlock();
if (is_done) {
var vm = globalThis.bunVM();
this.poll_ref.unref(vm);
this.clearData();
this.deinit();
}
}
if (!success) {
const err = this.onReject();
err.ensureStillAlive();
if (this.response.get()) |response_js| {
if (response_js.as(Response)) |response| {
const body = response.body;
if (body.value == .Locked) {
if (body.value.Locked.readable) |readable| {
readable.ptr.Bytes.onData(
.{
.err = .{ .JSValue = err },
},
bun.default_allocator,
);
return;
}
}
response.body.value.toErrorInstance(err, globalThis);
return;
}
}
globalThis.throwValue(err);
return;
}
if (this.readable_stream_ref.get()) |readable| {
if (readable.ptr == .Bytes) {
readable.ptr.Bytes.size_hint = this.getSizeHint();
// body can be marked as used but we still need to pipe the data
var scheduled_response_buffer = this.scheduled_response_buffer.list;
const chunk = scheduled_response_buffer.items;
if (this.result.has_more) {
readable.ptr.Bytes.onData(
.{
.temporary = bun.ByteList.initConst(chunk),
},
bun.default_allocator,
);
// clean for reuse later
this.scheduled_response_buffer.reset();
} else {
readable.ptr.Bytes.onData(
.{
.temporary_and_done = bun.ByteList.initConst(chunk),
},
bun.default_allocator,
);
}
return;
}
}
if (this.response.get()) |response_js| {
if (response_js.as(Response)) |response| {
const body = response.body;
if (body.value == .Locked) {
if (body.value.Locked.readable) |readable| {
if (readable.ptr == .Bytes) {
readable.ptr.Bytes.size_hint = this.getSizeHint();
var scheduled_response_buffer = this.scheduled_response_buffer.list;
const chunk = scheduled_response_buffer.items;
if (this.result.has_more) {
readable.ptr.Bytes.onData(
.{
.temporary = bun.ByteList.initConst(chunk),
},
bun.default_allocator,
);
// clean for reuse later
this.scheduled_response_buffer.reset();
} else {
readable.ptr.Bytes.onData(
.{
.temporary_and_done = bun.ByteList.initConst(chunk),
},
bun.default_allocator,
);
}
return;
}
} else {
response.body.value.Locked.size_hint = this.getSizeHint();
}
// we will reach here when not streaming
if (!this.result.has_more) {
var scheduled_response_buffer = this.scheduled_response_buffer.list;
this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice());
// done resolve body
var old = body.value;
var body_value = Body.Value{
.InternalBlob = .{
.bytes = scheduled_response_buffer.toManaged(bun.default_allocator),
},
};
response.body.value = body_value;
this.scheduled_response_buffer = .{
.allocator = this.memory_reporter.allocator(),
.list = .{
.items = &.{},
.capacity = 0,
},
};
if (old == .Locked) {
old.resolve(&response.body.value, this.global_this);
}
}
}
}
}
}
pub fn onProgressUpdate(this: *FetchTasklet) void {
JSC.markBinding(@src());
log("onProgressUpdate", .{});
if (this.is_waiting_body) {
return this.onBodyReceived();
}
// if we abort because of cert error
// we wait the Http Client because we already have the response
// we just need to deinit
const globalThis = this.global_this;
this.mutex.lock();
if (this.is_waiting_abort) {
// has_more will be false when the request is aborted/finished
if (this.result.has_more) {
this.mutex.unlock();
return;
}
this.mutex.unlock();
var poll_ref = this.poll_ref;
var vm = globalThis.bunVM();
poll_ref.unref(vm);
this.clearData();
this.deinit();
return;
}
var ref = this.promise;
const promise_value = ref.valueOrEmpty();
var poll_ref = this.poll_ref;
var vm = globalThis.bunVM();
if (promise_value.isEmptyOrUndefinedOrNull()) {
log("onProgressUpdate: promise_value is null", .{});
ref.strong.deinit();
this.has_schedule_callback.store(false, .Monotonic);
this.mutex.unlock();
poll_ref.unref(vm);
this.clearData();
this.deinit();
return;
}
if (this.result.certificate_info) |certificate_info| {
this.result.certificate_info = null;
defer certificate_info.deinit(bun.default_allocator);
// we receive some error
if (this.reject_unauthorized and !this.checkServerIdentity(certificate_info)) {
log("onProgressUpdate: aborted due certError", .{});
// we need to abort the request
const promise = promise_value.asAnyPromise().?;
const tracker = this.tracker;
const result = this.onReject();
result.ensureStillAlive();
promise_value.ensureStillAlive();
promise.reject(globalThis, result);
tracker.didDispatch(globalThis);
ref.strong.deinit();
this.has_schedule_callback.store(false, .Monotonic);
this.mutex.unlock();
if (this.is_waiting_abort) {
return;
}
// we are already done we can deinit
poll_ref.unref(vm);
this.clearData();
this.deinit();
return;
}
// everything ok
if (this.metadata == null) {
log("onProgressUpdate: metadata is null", .{});
this.has_schedule_callback.store(false, .Monotonic);
// cannot continue without metadata
this.mutex.unlock();
return;
}
}
const promise = promise_value.asAnyPromise().?;
const tracker = this.tracker;
tracker.willDispatch(globalThis);
defer {
log("onProgressUpdate: promise_value is not null", .{});
tracker.didDispatch(globalThis);
ref.strong.deinit();
this.has_schedule_callback.store(false, .Monotonic);
this.mutex.unlock();
if (!this.is_waiting_body) {
poll_ref.unref(vm);
this.clearData();
this.deinit();
}
}
const success = this.result.isSuccess();
const result = switch (success) {
true => this.onResolve(),
false => this.onReject(),
};
result.ensureStillAlive();
promise_value.ensureStillAlive();
switch (success) {
true => {
promise.resolve(globalThis, result);
},
false => {
promise.reject(globalThis, result);
},
}
}
pub fn checkServerIdentity(this: *FetchTasklet, certificate_info: HTTPClient.CertificateInfo) bool {
if (this.check_server_identity.get()) |check_server_identity| {
if (certificate_info.cert.len > 0) {
var cert = certificate_info.cert;
var cert_ptr = cert.ptr;
if (BoringSSL.d2i_X509(null, &cert_ptr, @intCast(cert.len))) |x509| {
defer BoringSSL.X509_free(x509);
const globalObject = this.global_this;
const js_cert = X509.toJS(x509, globalObject);
var hostname: bun.String = bun.String.create(certificate_info.hostname);
const js_hostname = hostname.toJS(globalObject);
const check_result = check_server_identity.callWithThis(globalObject, JSC.JSValue.jsUndefined(), &[_]JSC.JSValue{ js_hostname, js_cert });
// if check failed abort the request
if (check_result.isAnyError()) {
// mark to wait until deinit
this.is_waiting_abort = this.result.has_more;
check_result.ensureStillAlive();
check_result.protect();
this.abort_reason = check_result;
this.signal_store.aborted.store(true, .Monotonic);
this.tracker.didCancel(this.global_this);
// we need to abort the request
if (this.http != null) {
HTTPClient.http_thread.scheduleShutdown(this.http.?);
}
this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID;
return false;
}
return true;
}
}
}
this.result.fail = error.ERR_TLS_CERT_ALTNAME_INVALID;
return false;
}
pub fn onReject(this: *FetchTasklet) JSValue {
log("onReject", .{});
if (this.signal) |signal| {
this.signal = null;
signal.detach(this);
}
if (!this.abort_reason.isEmptyOrUndefinedOrNull()) {
return this.abort_reason;
}
if (this.result.isTimeout()) {
// Timeout without reason
return JSC.WebCore.AbortSignal.createTimeoutError(JSC.ZigString.static("The operation timed out"), &JSC.ZigString.Empty, this.global_this);
}
if (this.result.isAbort()) {
// Abort without reason
return JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.global_this);
}
var path: bun.String = undefined;
// some times we don't have metadata so we also check http.url
if (this.metadata) |metadata| {
path = bun.String.create(metadata.url);
} else if (this.http) |http| {
path = bun.String.create(http.url.href);
} else {
path = bun.String.empty;
}
const fetch_error = JSC.SystemError{
.code = bun.String.static(@errorName(this.result.fail)),
.message = switch (this.result.fail) {
error.ConnectionClosed => bun.String.static("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"),
error.FailedToOpenSocket => bun.String.static("Was there a typo in the url or port?"),
error.TooManyRedirects => bun.String.static("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"),
error.ConnectionRefused => bun.String.static("Unable to connect. Is the computer able to access the url?"),
else => bun.String.static("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"),
},
.path = path,
};
return fetch_error.toErrorInstance(this.global_this);
}
pub fn onReadableStreamAvailable(ctx: *anyopaque, readable: JSC.WebCore.ReadableStream) void {
const this = bun.cast(*FetchTasklet, ctx);
this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, this.global_this) catch .{};
}
pub fn onStartStreamingRequestBodyCallback(ctx: *anyopaque) JSC.WebCore.DrainResult {
const this = bun.cast(*FetchTasklet, ctx);
if (this.http) |http| {
http.enableBodyStreaming();
}
if (this.signal_store.aborted.load(.Monotonic)) {
return JSC.WebCore.DrainResult{
.aborted = {},
};
}
this.mutex.lock();
defer this.mutex.unlock();
const size_hint = this.getSizeHint();
var scheduled_response_buffer = this.scheduled_response_buffer.list;
// This means we have received part of the body but not the whole thing
if (scheduled_response_buffer.items.len > 0) {
this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice());
this.scheduled_response_buffer = .{
.allocator = this.memory_reporter.allocator(),
.list = .{
.items = &.{},
.capacity = 0,
},
};
return .{
.owned = .{
.list = scheduled_response_buffer.toManaged(bun.default_allocator),
.size_hint = size_hint,
},
};
}
return .{
.estimated_size = size_hint,
};
}
fn getSizeHint(this: *FetchTasklet) Blob.SizeType {
return switch (this.body_size) {
.content_length => @truncate(this.body_size.content_length),
.total_received => @truncate(this.body_size.total_received),
else => 0,
};
}
fn toBodyValue(this: *FetchTasklet) Body.Value {
if (this.is_waiting_body) {
const response = Body.Value{
.Locked = .{
.size_hint = this.getSizeHint(),
.task = this,
.global = this.global_this,
.onStartStreaming = FetchTasklet.onStartStreamingRequestBodyCallback,
.onReadableStreamAvailable = FetchTasklet.onReadableStreamAvailable,
},
};
return response;
}
var scheduled_response_buffer = this.scheduled_response_buffer.list;
this.memory_reporter.discard(scheduled_response_buffer.allocatedSlice());
const response = Body.Value{
.InternalBlob = .{
.bytes = scheduled_response_buffer.toManaged(bun.default_allocator),
},
};
this.scheduled_response_buffer = .{
.allocator = this.memory_reporter.allocator(),
.list = .{
.items = &.{},
.capacity = 0,
},
};
return response;
}
fn toResponse(this: *FetchTasklet, allocator: std.mem.Allocator) Response {
log("toResponse", .{});
std.debug.assert(this.metadata != null);
// at this point we always should have metadata
var metadata = this.metadata.?;
const http_response = metadata.response;
this.is_waiting_body = this.result.has_more;
return Response{
.allocator = allocator,
.url = bun.String.createAtomIfPossible(metadata.url),
.status_text = bun.String.createAtomIfPossible(http_response.status),
.redirected = this.result.redirected,
.body = .{
.init = .{
.headers = FetchHeaders.createFromPicoHeaders(http_response.headers),
.status_code = @as(u16, @truncate(http_response.status_code)),
},
.value = this.toBodyValue(),
},
};
}
pub fn onResolve(this: *FetchTasklet) JSValue {
log("onResolve", .{});
const allocator = bun.default_allocator;
var response = allocator.create(Response) catch unreachable;
response.* = this.toResponse(allocator);
const response_js = Response.makeMaybePooled(@as(js.JSContextRef, this.global_this), response);
response_js.ensureStillAlive();
this.response = JSC.Strong.create(response_js, this.global_this);
return response_js;
}
pub fn get(
allocator: std.mem.Allocator,
globalThis: *JSC.JSGlobalObject,
promise: JSC.JSPromise.Strong,
fetch_options: FetchOptions,
) !*FetchTasklet {
var jsc_vm = globalThis.bunVM();
var fetch_tasklet = try allocator.create(FetchTasklet);
fetch_tasklet.* = .{
.mutex = Mutex.init(),
.scheduled_response_buffer = .{
.allocator = fetch_options.memory_reporter.allocator(),
.list = .{
.items = &.{},
.capacity = 0,
},
},
.response_buffer = MutableString{
.allocator = fetch_options.memory_reporter.allocator(),
.list = .{
.items = &.{},
.capacity = 0,
},
},
.http = try allocator.create(HTTPClient.AsyncHTTP),
.javascript_vm = jsc_vm,
.request_body = fetch_options.body,
.global_this = globalThis,
.request_headers = fetch_options.headers,
.promise = promise,
.url_proxy_buffer = fetch_options.url_proxy_buffer,
.signal = fetch_options.signal,
.hostname = fetch_options.hostname,
.tracker = JSC.AsyncTaskTracker.init(jsc_vm),
.memory_reporter = fetch_options.memory_reporter,
.check_server_identity = fetch_options.check_server_identity,
.reject_unauthorized = fetch_options.reject_unauthorized,
};
fetch_tasklet.signals = fetch_tasklet.signal_store.to();
fetch_tasklet.tracker.didSchedule(globalThis);
if (fetch_tasklet.request_body.store()) |store| {
store.ref();
}
var proxy: ?ZigURL = null;
if (fetch_options.proxy) |proxy_opt| {
if (!proxy_opt.isEmpty()) { //if is empty just ignore proxy
proxy = fetch_options.proxy orelse jsc_vm.bundler.env.getHttpProxy(fetch_options.url);
}
} else {
proxy = jsc_vm.bundler.env.getHttpProxy(fetch_options.url);
}
if (fetch_tasklet.check_server_identity.has() and fetch_tasklet.reject_unauthorized) {
fetch_tasklet.signal_store.cert_errors.store(true, .Monotonic);
} else {
fetch_tasklet.signals.cert_errors = null;
// we use aborted to signal that we should abort reject_unauthorized after check with check_server_identity
if (fetch_tasklet.signal == null) {
fetch_tasklet.signals.aborted = null;
}
}
fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init(
fetch_options.memory_reporter.allocator(),
fetch_options.method,
fetch_options.url,
fetch_options.headers.entries,
fetch_options.headers.buf.items,
&fetch_tasklet.response_buffer,
fetch_tasklet.request_body.slice(),
fetch_options.timeout,
HTTPClient.HTTPClientResult.Callback.New(
*FetchTasklet,
FetchTasklet.callback,
).init(
fetch_tasklet,
),
proxy,
fetch_options.hostname,
fetch_options.redirect_type,
fetch_tasklet.signals,
);
if (fetch_options.redirect_type != FetchRedirect.follow) {
fetch_tasklet.http.?.client.remaining_redirect_count = 0;
}
fetch_tasklet.http.?.client.disable_timeout = fetch_options.disable_timeout;
fetch_tasklet.http.?.client.verbose = fetch_options.verbose;
fetch_tasklet.http.?.client.disable_keepalive = fetch_options.disable_keepalive;
fetch_tasklet.http.?.client.disable_decompression = fetch_options.disable_decompression;
fetch_tasklet.http.?.client.reject_unauthorized = fetch_options.reject_unauthorized;
// we wanna to return after headers are received
fetch_tasklet.signal_store.header_progress.store(true, .Monotonic);
if (fetch_tasklet.request_body == .Sendfile) {
std.debug.assert(fetch_options.url.isHTTP());
std.debug.assert(fetch_options.proxy == null);
fetch_tasklet.http.?.request_body = .{ .sendfile = fetch_tasklet.request_body.Sendfile };
}
if (fetch_tasklet.signal) |signal| {
fetch_tasklet.signal = signal.listen(FetchTasklet, fetch_tasklet, FetchTasklet.abortListener);
}
return fetch_tasklet;
}
pub fn abortListener(this: *FetchTasklet, reason: JSValue) void {
log("abortListener", .{});
reason.ensureStillAlive();
this.abort_reason = reason;
reason.protect();
this.signal_store.aborted.store(true, .Monotonic);
this.tracker.didCancel(this.global_this);
if (this.http != null) {
HTTPClient.http_thread.scheduleShutdown(this.http.?);
}
}
const FetchOptions = struct {
method: Method,
headers: Headers,
body: HTTPRequestBody,
timeout: usize,
disable_timeout: bool,
disable_keepalive: bool,
disable_decompression: bool,
reject_unauthorized: bool,
url: ZigURL,
verbose: bool = false,
redirect_type: FetchRedirect = FetchRedirect.follow,
proxy: ?ZigURL = null,
url_proxy_buffer: []const u8 = "",
signal: ?*JSC.WebCore.AbortSignal = null,
globalThis: ?*JSGlobalObject,
// Custom Hostname
hostname: ?[]u8 = null,
memory_reporter: *JSC.MemoryReportingAllocator,
check_server_identity: JSC.Strong = .{},
};
pub fn queue(
allocator: std.mem.Allocator,
global: *JSGlobalObject,
fetch_options: FetchOptions,
promise: JSC.JSPromise.Strong,
) !*FetchTasklet {
try HTTPClient.HTTPThread.init();
var node = try get(
allocator,
global,
promise,
fetch_options,
);
var batch = NetworkThread.Batch{};
node.http.?.schedule(allocator, &batch);
node.poll_ref.ref(global.bunVM());
HTTPClient.http_thread.schedule(batch);
return node;
}
pub fn callback(task: *FetchTasklet, result: HTTPClient.HTTPClientResult) void {
task.mutex.lock();
defer task.mutex.unlock();
log("callback success {} has_more {} bytes {}", .{ result.isSuccess(), result.has_more, result.body.?.list.items.len });
task.result = result;
// metadata should be provided only once so we preserve it until we consume it
if (result.metadata) |metadata| {
log("added callback metadata", .{});
std.debug.assert(task.metadata == null);
task.metadata = metadata;
}
task.body_size = result.body_size;
const success = result.isSuccess();
task.response_buffer = result.body.?.*;
if (success) {
_ = task.scheduled_response_buffer.write(task.response_buffer.list.items) catch @panic("OOM");
}
// reset for reuse
task.response_buffer.reset();
if (task.has_schedule_callback.compareAndSwap(false, true, .Acquire, .Monotonic)) |has_schedule_callback| {
if (has_schedule_callback) {
return;
}
}
task.javascript_vm.eventLoop().enqueueTaskConcurrent(task.concurrent_task.from(task, .manual_deinit));
}
};
fn dataURLResponse(
_data_url: DataURL,
globalThis: *JSGlobalObject,
allocator: std.mem.Allocator,
) JSValue {
var data_url = _data_url;
const data = data_url.decodeData(allocator) catch {
const err = JSC.createError(globalThis, "failed to fetch the data URL", .{});
return JSPromise.rejectedPromiseValue(globalThis, err);
};
var blob = Blob.init(data, allocator, globalThis);
var allocated = false;
const mime_type = bun.HTTP.MimeType.init(data_url.mime_type, allocator, &allocated);
blob.content_type = mime_type.value;
if (allocated) {
blob.content_type_allocated = true;
}
var response = allocator.create(Response) catch @panic("out of memory");
response.* = Response{
.body = Body{
.init = Body.Init{
.status_code = 200,
},
.value = .{
.Blob = blob,
},
},
.allocator = allocator,
.status_text = bun.String.createAtom("OK"),
.url = data_url.url.dupeRef(),
};
return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis));
}
pub export fn Bun__fetch(
ctx: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) callconv(.C) JSC.JSValue {
JSC.markBinding(@src());
const globalThis = ctx.ptr();
const arguments = callframe.arguments(2);
var exception_val = [_]JSC.C.JSValueRef{null};
var exception: JSC.C.ExceptionRef = &exception_val;
var memory_reporter = bun.default_allocator.create(JSC.MemoryReportingAllocator) catch @panic("out of memory");
var free_memory_reporter = false;
var allocator = memory_reporter.wrap(bun.default_allocator);
defer {
if (exception.* != null) {
free_memory_reporter = true;
ctx.throwValue(JSC.JSValue.c(exception.*));
}
memory_reporter.report(globalThis.vm());
if (free_memory_reporter) bun.default_allocator.destroy(memory_reporter);
}
if (arguments.len == 0) {
const err = JSC.toTypeError(.ERR_MISSING_ARGS, fetch_error_no_args, .{}, ctx);
return JSPromise.rejectedPromiseValue(globalThis, err);
}
var headers: ?Headers = null;
var method = Method.GET;
var script_ctx = globalThis.bunVM();
var args = JSC.Node.ArgumentsSlice.init(script_ctx, arguments.ptr[0..arguments.len]);
var url = ZigURL{};
var first_arg = args.nextEat().?;
// We must always get the Body before the Headers That way, we can set
// the Content-Type header from the Blob if no Content-Type header is
// set in the Headers
//
// which is important for FormData.
// https://github.com/oven-sh/bun/issues/2264
//
var body: AnyBlob = AnyBlob{
.Blob = .{},
};
var disable_timeout = false;
var disable_keepalive = false;
var disable_decompression = false;
var verbose = script_ctx.log.level.atLeast(.debug);
var proxy: ?ZigURL = null;
var redirect_type: FetchRedirect = FetchRedirect.follow;
var signal: ?*JSC.WebCore.AbortSignal = null;
// Custom Hostname
var hostname: ?[]u8 = null;
var url_proxy_buffer: []const u8 = undefined;
var is_file_url = false;
var reject_unauthorized = true;
var check_server_identity: JSValue = .zero;
// TODO: move this into a DRYer implementation
// The status quo is very repetitive and very bug prone
if (first_arg.as(Request)) |request| {
request.ensureURL() catch unreachable;
if (request.url.isEmpty()) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
free_memory_reporter = true;
return JSPromise.rejectedPromiseValue(globalThis, err);
}
if (request.url.hasPrefixComptime("data:")) {
var url_slice = request.url.toUTF8WithoutRef(allocator);
defer url_slice.deinit();
var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch {
const err = JSC.createError(globalThis, "failed to fetch the data URL", .{});
return JSPromise.rejectedPromiseValue(globalThis, err);
};
data_url.url = request.url;
return dataURLResponse(data_url, globalThis, allocator);
}
url = ZigURL.fromString(allocator, request.url) catch {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
free_memory_reporter = true;
return JSPromise.rejectedPromiseValue(
globalThis,
err,
);
};
is_file_url = url.isFile();
url_proxy_buffer = url.href;
if (!is_file_url) {
if (args.nextEat()) |options| {
if (options.isObject() or options.jsType() == .DOMWrapper) {
if (options.fastGet(ctx.ptr(), .method)) |method_| {
var slice_ = method_.toSlice(ctx.ptr(), allocator);
defer slice_.deinit();
method = Method.which(slice_.slice()) orelse .GET;
} else {
method = request.method;
}
if (options.fastGet(ctx.ptr(), .body)) |body__| {
if (Body.Value.fromJS(ctx.ptr(), body__)) |body_const| {
var body_value = body_const;
// TODO: buffer ReadableStream?
// we have to explicitly check for InternalBlob
body = body_value.useAsAnyBlob();
} else {
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
// an error was thrown
return JSC.JSValue.jsUndefined();
}
} else {
body = request.body.value.useAsAnyBlob();
}
if (options.fastGet(ctx.ptr(), .headers)) |headers_| {
if (headers_.as(FetchHeaders)) |headers__| {
if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable;
// TODO: make this one pass
} else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| {
if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable;
headers__.deref();
} else if (request.headers) |head| {
if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable;
}
} else if (request.headers) |head| {
headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable;
}
if (options.get(ctx, "timeout")) |timeout_value| {
if (timeout_value.isBoolean()) {
disable_timeout = !timeout_value.asBoolean();
} else if (timeout_value.isNumber()) {
disable_timeout = timeout_value.to(i32) == 0;
}
}
if (options.getOptionalEnum(ctx, "redirect", FetchRedirect) catch {
return .zero;
}) |redirect_value| {
redirect_type = redirect_value;
}
if (options.get(ctx, "keepalive")) |keepalive_value| {
if (keepalive_value.isBoolean()) {
disable_keepalive = !keepalive_value.asBoolean();
} else if (keepalive_value.isNumber()) {
disable_keepalive = keepalive_value.to(i32) == 0;
}
}
if (options.get(globalThis, "verbose")) |verb| {
verbose = verb.toBoolean();
}
if (options.get(globalThis, "signal")) |signal_arg| {
if (signal_arg.as(JSC.WebCore.AbortSignal)) |signal_| {
_ = signal_.ref();
signal = signal_;
}
}
if (options.get(ctx, "decompress")) |decompress| {
if (decompress.isBoolean()) {
disable_decompression = !decompress.asBoolean();
} else if (decompress.isNumber()) {
disable_keepalive = decompress.to(i32) == 0;
}
}
if (options.get(ctx, "tls")) |tls| {
if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) {
if (tls.get(ctx, "rejectUnauthorized")) |reject| {
if (reject.isBoolean()) {
reject_unauthorized = reject.asBoolean();
} else if (reject.isNumber()) {
reject_unauthorized = reject.to(i32) != 0;
}
}
if (tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| {
if (checkServerIdentity.isCell() and checkServerIdentity.isCallable(globalThis.vm())) {
check_server_identity = checkServerIdentity;
}
}
}
}
if (options.get(globalThis, "proxy")) |proxy_arg| {
if (proxy_arg.isString() and proxy_arg.getLength(ctx) > 0) {
var href = JSC.URL.hrefFromJS(proxy_arg, globalThis);
if (href.tag == .Dead) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
allocator.free(url_proxy_buffer);
return JSPromise.rejectedPromiseValue(globalThis, err);
}
defer href.deref();
var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch {
globalThis.throwOutOfMemory();
return .zero;
};
url = ZigURL.parse(buffer[0..url.href.len]);
is_file_url = url.isFile();
proxy = ZigURL.parse(buffer[url.href.len..]);
allocator.free(url_proxy_buffer);
url_proxy_buffer = buffer;
}
}
}
} else {
method = request.method;
body = request.body.value.useAsAnyBlob();
if (request.headers) |head| {
if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(head, allocator, .{ .body = &body }) catch unreachable;
}
if (request.signal) |signal_| {
_ = signal_.ref();
signal = signal_;
}
}
}
} else if (bun.String.tryFromJS(first_arg, globalThis)) |str| {
if (str.isEmpty()) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
return JSPromise.rejectedPromiseValue(globalThis, err);
}
if (str.hasPrefixComptime("data:")) {
var url_slice = str.toUTF8WithoutRef(allocator);
defer url_slice.deinit();
var data_url = DataURL.parseWithoutCheck(url_slice.slice()) catch {
const err = JSC.createError(globalThis, "failed to fetch the data URL", .{});
return JSPromise.rejectedPromiseValue(globalThis, err);
};
data_url.url = str;
return dataURLResponse(data_url, globalThis, allocator);
}
url = ZigURL.fromString(allocator, str) catch {
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx);
return JSPromise.rejectedPromiseValue(globalThis, err);
};
url_proxy_buffer = url.href;
is_file_url = url.isFile();
if (!is_file_url) {
if (args.nextEat()) |options| {
if (options.isObject() or options.jsType() == .DOMWrapper) {
if (options.fastGet(ctx.ptr(), .method)) |method_| {
var slice_ = method_.toSlice(ctx.ptr(), allocator);
defer slice_.deinit();
method = Method.which(slice_.slice()) orelse .GET;
}
if (options.fastGet(ctx.ptr(), .body)) |body__| {
if (Body.Value.fromJS(ctx.ptr(), body__)) |body_const| {
var body_value = body_const;
// TODO: buffer ReadableStream?
// we have to explicitly check for InternalBlob
body = body_value.useAsAnyBlob();
} else {
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
// an error was thrown
return JSC.JSValue.jsUndefined();
}
}
if (options.fastGet(ctx.ptr(), .headers)) |headers_| {
if (headers_.as(FetchHeaders)) |headers__| {
if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable;
// TODO: make this one pass
} else if (FetchHeaders.createFromJS(ctx.ptr(), headers_)) |headers__| {
defer headers__.deref();
if (headers__.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| {
if (hostname) |host| {
allocator.free(host);
}
hostname = _hostname.toOwnedSliceZ(allocator) catch unreachable;
}
headers = Headers.from(headers__, allocator, .{ .body = &body }) catch unreachable;
} else {
// Converting the headers failed; return null and
// let the set exception get thrown
return .zero;
}
}
if (options.get(ctx, "timeout")) |timeout_value| {
if (timeout_value.isBoolean()) {
disable_timeout = !timeout_value.asBoolean();
} else if (timeout_value.isNumber()) {
disable_timeout = timeout_value.to(i32) == 0;
}
}
if (options.getOptionalEnum(ctx, "redirect", FetchRedirect) catch {
return .zero;
}) |redirect_value| {
redirect_type = redirect_value;
}
if (options.get(ctx, "keepalive")) |keepalive_value| {
if (keepalive_value.isBoolean()) {
disable_keepalive = !keepalive_value.asBoolean();
} else if (keepalive_value.isNumber()) {
disable_keepalive = keepalive_value.to(i32) == 0;
}
}
if (options.get(globalThis, "verbose")) |verb| {
verbose = verb.toBoolean();
}
if (options.get(globalThis, "signal")) |signal_arg| {
if (signal_arg.as(JSC.WebCore.AbortSignal)) |signal_| {
_ = signal_.ref();
signal = signal_;
}
}
if (options.get(ctx, "decompress")) |decompress| {
if (decompress.isBoolean()) {
disable_decompression = !decompress.asBoolean();
} else if (decompress.isNumber()) {
disable_keepalive = decompress.to(i32) == 0;
}
}
if (options.get(ctx, "tls")) |tls| {
if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) {
if (tls.get(ctx, "rejectUnauthorized")) |reject| {
if (reject.isBoolean()) {
reject_unauthorized = reject.asBoolean();
} else if (reject.isNumber()) {
reject_unauthorized = reject.to(i32) != 0;
}
}
if (tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| {
if (checkServerIdentity.isCell() and checkServerIdentity.isCallable(globalThis.vm())) {
check_server_identity = checkServerIdentity;
}
}
}
}
if (options.getTruthy(globalThis, "proxy")) |proxy_arg| {
if (proxy_arg.isString() and proxy_arg.getLength(globalThis) > 0) {
var href = JSC.URL.hrefFromJS(proxy_arg, globalThis);
if (href.tag == .Dead) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
hostname = null;
}
allocator.free(url_proxy_buffer);
free_memory_reporter = true;
return JSPromise.rejectedPromiseValue(globalThis, err);
}
defer href.deref();
var buffer = std.fmt.allocPrint(allocator, "{s}{}", .{ url_proxy_buffer, href }) catch {
globalThis.throwOutOfMemory();
return .zero;
};
url = ZigURL.parse(buffer[0..url.href.len]);
proxy = ZigURL.parse(buffer[url.href.len..]);
allocator.free(url_proxy_buffer);
url_proxy_buffer = buffer;
}
}
}
}
}
} else {
const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, first_arg.asRef()));
const err = JSC.toTypeError(.ERR_INVALID_ARG_TYPE, "{s}", .{fetch_error}, ctx);
exception.* = err.asObjectRef();
return .zero;
}
if (url.isEmpty()) {
free_memory_reporter = true;
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx);
return JSPromise.rejectedPromiseValue(globalThis, err);
}
// This is not 100% correct.
// We don't pass along headers, we ignore method, we ignore status code...
// But it's better than status quo.
if (is_file_url) {
defer allocator.free(url_proxy_buffer);
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
const PercentEncoding = @import("../../url.zig").PercentEncoding;
var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined;
var stream = std.io.fixedBufferStream(&path_buf2);
const url_path_decoded = path_buf2[0 .. PercentEncoding.decode(
@TypeOf(&stream.writer()),
&stream.writer(),
url.path,
) catch {
globalThis.throwOutOfMemory();
return .zero;
}];
const temp_file_path = bun.path.joinAbsStringBuf(
globalThis.bunVM().bundler.fs.top_level_dir,
&path_buf,
&[_]string{
globalThis.bunVM().main,
"../",
url_path_decoded,
},
.auto,
);
var file_url_string = JSC.URL.fileURLFromString(bun.String.fromUTF8(temp_file_path));
defer file_url_string.deref();
const bun_file = Blob.findOrCreateFileFromPath(
.{
.path = .{
.string = bun.PathString.init(
temp_file_path,
),
},
},
globalThis,
);
var response = bun.default_allocator.create(Response) catch @panic("out of memory");
response.* = Response{
.body = Body{
.init = Body.Init{
.status_code = 200,
},
.value = .{ .Blob = bun_file },
},
.allocator = bun.default_allocator,
.url = file_url_string.clone(),
};
return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis));
}
if (url.protocol.len > 0) {
if (!(url.isHTTP() or url.isHTTPS())) {
defer allocator.free(url_proxy_buffer);
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "protocol must be http: or https:", .{}, ctx);
free_memory_reporter = true;
return JSPromise.rejectedPromiseValue(globalThis, err);
}
}
if (!method.hasRequestBody() and body.size() > 0) {
defer allocator.free(url_proxy_buffer);
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_unexpected_body, .{}, ctx);
free_memory_reporter = true;
return JSPromise.rejectedPromiseValue(globalThis, err);
}
if (headers == null and body.size() > 0 and body.hasContentTypeFromUser()) {
headers = Headers.from(
null,
allocator,
.{ .body = &body },
) catch unreachable;
}
var http_body = FetchTasklet.HTTPRequestBody{
.AnyBlob = body,
};
if (body.needsToReadFile()) {
prepare_body: {
const opened_fd_res: JSC.Node.Maybe(bun.FileDescriptor) = switch (body.Blob.store.?.data.file.pathlike) {
.fd => |fd| bun.sys.dup(fd),
.path => |path| bun.sys.open(path.sliceZ(&globalThis.bunVM().nodeFS().sync_error_buf), std.os.O.RDONLY | std.os.O.NOCTTY, 0),
};
const opened_fd = switch (opened_fd_res) {
.err => |err| {
allocator.free(url_proxy_buffer);
const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
body.detach();
if (headers) |*headers_| {
headers_.buf.deinit(allocator);
headers_.entries.deinit(allocator);
}
free_memory_reporter = true;
return rejected_value;
},
.result => |fd| fd,
};
if (proxy == null and bun.HTTP.Sendfile.isEligible(url)) {
use_sendfile: {
const stat: bun.Stat = switch (bun.sys.fstat(opened_fd)) {
.result => |result| result,
// bail out for any reason
.err => break :use_sendfile,
};
if (Environment.isMac) {
// macOS only supports regular files for sendfile()
if (!bun.isRegularFile(stat.mode)) {
break :use_sendfile;
}
}
// if it's < 32 KB, it's not worth it
if (stat.size < 32 * 1024) {
break :use_sendfile;
}
const original_size = body.Blob.size;
const stat_size = @as(Blob.SizeType, @intCast(stat.size));
const blob_size = if (bun.isRegularFile(stat.mode))
stat_size
else
@min(original_size, stat_size);
http_body = .{
.Sendfile = .{
.fd = opened_fd,
.remain = body.Blob.offset + original_size,
.offset = body.Blob.offset,
.content_size = blob_size,
},
};
if (bun.isRegularFile(stat.mode)) {
http_body.Sendfile.offset = @min(http_body.Sendfile.offset, stat_size);
http_body.Sendfile.remain = @min(@max(http_body.Sendfile.remain, http_body.Sendfile.offset), stat_size) -| http_body.Sendfile.offset;
}
body.detach();
break :prepare_body;
}
}
// TODO: make this async + lazy
const res = JSC.Node.NodeFS.readFile(
globalThis.bunVM().nodeFS(),
.{
.encoding = .buffer,
.path = .{ .fd = opened_fd },
.offset = body.Blob.offset,
.max_size = body.Blob.size,
},
.sync,
);
if (body.Blob.store.?.data.file.pathlike == .path) {
_ = bun.sys.close(opened_fd);
}
switch (res) {
.err => |err| {
allocator.free(url_proxy_buffer);
free_memory_reporter = true;
const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis));
body.detach();
if (headers) |*headers_| {
headers_.buf.deinit(allocator);
headers_.entries.deinit(allocator);
}
return rejected_value;
},
.result => |result| {
body.detach();
body.from(std.ArrayList(u8).fromOwnedSlice(allocator, @constCast(result.slice())));
http_body = .{ .AnyBlob = body };
},
}
}
}
// Only create this after we have validated all the input.
// or else we will leak it
var promise = JSPromise.Strong.init(globalThis);
const promise_val = promise.value();
// var resolve = FetchTasklet.FetchResolver.Class.make(ctx: js.JSContextRef, ptr: *ZigType)
_ = FetchTasklet.queue(
allocator,
globalThis,
.{
.method = method,
.url = url,
.headers = headers orelse Headers{
.allocator = allocator,
},
.body = http_body,
.timeout = std.time.ns_per_hour,
.disable_keepalive = disable_keepalive,
.disable_timeout = disable_timeout,
.disable_decompression = disable_decompression,
.reject_unauthorized = reject_unauthorized,
.redirect_type = redirect_type,
.verbose = verbose,
.proxy = proxy,
.url_proxy_buffer = url_proxy_buffer,
.signal = signal,
.globalThis = globalThis,
.hostname = hostname,
.memory_reporter = memory_reporter,
.check_server_identity = if (check_server_identity.isEmptyOrUndefinedOrNull()) .{} else JSC.Strong.create(check_server_identity, globalThis),
},
// Pass the Strong value instead of creating a new one, or else we
// will leak it
// see https://github.com/oven-sh/bun/issues/2985
promise,
) catch unreachable;
return promise_val;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
pub const Headers = struct {
pub usingnamespace HTTPClient.Headers;
entries: Headers.Entries = .{},
buf: std.ArrayListUnmanaged(u8) = .{},
allocator: std.mem.Allocator,
pub fn asStr(this: *const Headers, ptr: Api.StringPointer) []const u8 {
return if (ptr.offset + ptr.length <= this.buf.items.len)
this.buf.items[ptr.offset..][0..ptr.length]
else
"";
}
pub const Options = struct {
body: ?*const AnyBlob = null,
};
pub fn from(fetch_headers_ref: ?*FetchHeaders, allocator: std.mem.Allocator, options: Options) !Headers {
var header_count: u32 = 0;
var buf_len: u32 = 0;
if (fetch_headers_ref) |headers_ref|
headers_ref.count(&header_count, &buf_len);
var headers = Headers{
.entries = .{},
.buf = .{},
.allocator = allocator,
};
const buf_len_before_content_type = buf_len;
const needs_content_type = brk: {
if (options.body) |body| {
if (body.hasContentTypeFromUser() and (fetch_headers_ref == null or !fetch_headers_ref.?.fastHas(.ContentType))) {
header_count += 1;
buf_len += @as(u32, @truncate(body.contentType().len + "Content-Type".len));
break :brk true;
}
}
break :brk false;
};
headers.entries.ensureTotalCapacity(allocator, header_count) catch unreachable;
headers.entries.len = header_count;
headers.buf.ensureTotalCapacityPrecise(allocator, buf_len) catch unreachable;
headers.buf.items.len = buf_len;
var sliced = headers.entries.slice();
var names = sliced.items(.name);
var values = sliced.items(.value);
if (fetch_headers_ref) |headers_ref|
headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr);
// TODO: maybe we should send Content-Type header first instead of last?
if (needs_content_type) {
bun.copy(u8, headers.buf.items[buf_len_before_content_type..], "Content-Type");
names[header_count - 1] = .{
.offset = buf_len_before_content_type,
.length = "Content-Type".len,
};
bun.copy(u8, headers.buf.items[buf_len_before_content_type + "Content-Type".len ..], options.body.?.contentType());
values[header_count - 1] = .{
.offset = buf_len_before_content_type + @as(u32, "Content-Type".len),
.length = @as(u32, @truncate(options.body.?.contentType().len)),
};
}
return headers;
}
};