diff options
Diffstat (limited to 'src/bun.js')
-rw-r--r-- | src/bun.js/api/JSBundler.zig | 7 | ||||
-rw-r--r-- | src/bun.js/api/JSTranspiler.zig | 7 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 1 | ||||
-rw-r--r-- | src/bun.js/node/types.zig | 18 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 49 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 323 |
6 files changed, 337 insertions, 68 deletions
diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 741d956bf..8e85f1190 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -61,6 +61,7 @@ pub const JSBundler = struct { code_splitting: bool = false, minify: Minify = .{}, server_components: ServerComponents = ServerComponents{}, + no_macros: bool = false, names: Names = .{}, external: bun.StringSet = bun.StringSet.init(bun.default_allocator), @@ -188,6 +189,12 @@ pub const JSBundler = struct { } } + if (config.getTruthy(globalThis, "macros")) |macros_flag| { + if (!macros_flag.coerce(bool, globalThis)) { + this.no_macros = true; + } + } + if (try config.getOptionalEnum(globalThis, "target", options.Target)) |target| { this.target = target; } diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index 8a59f59e7..c58029c5e 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -75,6 +75,7 @@ const TranspilerOptions = struct { minify_whitespace: bool = false, minify_identifiers: bool = false, minify_syntax: bool = false, + no_macros: bool = false, }; // Mimalloc gets unstable if we try to move this to a different thread @@ -479,6 +480,10 @@ fn transformOptionsFromJSC(globalObject: JSC.C.JSContextRef, temp_allocator: std if (object.getIfPropertyExists(globalThis, "macro")) |macros| { macros: { if (macros.isUndefinedOrNull()) break :macros; + if (macros.isBoolean()) { + transpiler.no_macros = !macros.asBoolean(); + break :macros; + } const kind = macros.jsType(); const is_object = kind.isObject(); if (!(kind.isStringLike() or is_object)) { @@ -775,7 +780,7 @@ pub fn constructor( globalThis.throwError(err, "Error creating transpiler"); return null; }; - + bundler.options.no_macros = transpiler_options.no_macros; bundler.configureLinkerWithAutoJSX(false); bundler.options.env.behavior = .disable; bundler.configureDefines() catch |err| { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 4a1fcbcb1..5c158a4fb 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -392,6 +392,7 @@ pub const VirtualMachine = struct { macros: MacroMap, macro_entry_points: std.AutoArrayHashMap(i32, *MacroEntryPoint), macro_mode: bool = false, + no_macros: bool = false, has_any_macro_remappings: bool = false, is_from_devserver: bool = false, diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index bbe2ea654..f090b3a12 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1775,17 +1775,19 @@ pub const Path = struct { var path_slice: JSC.ZigString.Slice = args_ptr[0].toSlice(globalThis, heap_allocator); defer path_slice.deinit(); var path = path_slice.slice(); - var path_name = Fs.PathName.init(path); - var root = JSC.ZigString.init(path_name.dir); - const is_absolute = (isWindows and isZigStringAbsoluteWindows(root)) or (!isWindows and path_name.dir.len > 0 and path_name.dir[0] == '/'); - + var path_name = Fs.NodeJSPathName.init(path); var dir = JSC.ZigString.init(path_name.dir); + const is_absolute = (isWindows and isZigStringAbsoluteWindows(dir)) or (!isWindows and path.len > 0 and path[0] == '/'); + + // if its not absolute root must be empty + var root = JSC.ZigString.Empty; if (is_absolute) { - root = JSC.ZigString.Empty; - if (path_name.dir.len == 0) - dir = JSC.ZigString.init(if (isWindows) std.fs.path.sep_str_windows else std.fs.path.sep_str_posix); + root = JSC.ZigString.init(if (isWindows) std.fs.path.sep_str_windows else std.fs.path.sep_str_posix); + // if is absolute and dir is empty, then dir = root + if (path_name.dir.len == 0) { + dir = root; + } } - var base = JSC.ZigString.init(path_name.base); var name_ = JSC.ZigString.init(path_name.filename); var ext = JSC.ZigString.init(path_name.ext); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 591150e12..a5d3c968d 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -85,6 +85,7 @@ pub const Blob = struct { store: ?*Store = null, content_type: string = "", content_type_allocated: bool = false, + content_type_was_set: bool = false, /// JavaScriptCore strings are either latin1 or UTF-16 /// When UTF-16, they're nearly always due to non-ascii characters @@ -111,6 +112,10 @@ pub const Blob = struct { return bun.FormData.AsyncFormData.init(this.allocator orelse bun.default_allocator, encoding) catch unreachable; } + pub fn hasContentTypeFromUser(this: *const Blob) bool { + return this.content_type_was_set or (this.store != null and this.store.?.data == .file); + } + const FormDataContext = struct { allocator: std.mem.Allocator, joiner: StringJoiner, @@ -228,6 +233,7 @@ pub const Blob = struct { var blob = Blob.initWithStore(store, globalThis); blob.content_type = store.mime_type.value; + blob.content_type_was_set = true; return blob; } @@ -268,6 +274,7 @@ pub const Blob = struct { var blob = Blob.initWithStore(store, globalThis); blob.content_type = std.fmt.allocPrint(allocator, "multipart/form-data; boundary=\"{s}\"", .{boundary}) catch unreachable; blob.content_type_allocated = true; + blob.content_type_was_set = true; return blob; } @@ -288,7 +295,7 @@ pub const Blob = struct { export fn Blob__dupe(ptr: *anyopaque) *Blob { var this = bun.cast(*Blob, ptr); var new = bun.default_allocator.create(Blob) catch unreachable; - new.* = this.dupe(); + new.* = this.dupeWithContentType(true); new.allocator = bun.default_allocator; return new; } @@ -2527,6 +2534,7 @@ pub const Blob = struct { blob.content_type = content_type; blob.content_type_allocated = content_type_was_allocated; + blob.content_type_was_set = this.content_type_was_set or content_type_was_allocated; var blob_ = allocator.create(Blob) catch unreachable; blob_.* = blob; @@ -2548,13 +2556,13 @@ pub const Blob = struct { ) callconv(.C) JSValue { if (this.content_type.len > 0) { if (this.content_type_allocated) { - return ZigString.init(this.content_type).toValue(globalThis); + return ZigString.init(this.content_type).toValueGC(globalThis); } return ZigString.init(this.content_type).toValueGC(globalThis); } if (this.store) |store| { - return ZigString.init(store.mime_type.value).toValue(globalThis); + return ZigString.init(store.mime_type.value).toValueGC(globalThis); } return ZigString.Empty.toValue(globalThis); @@ -2754,6 +2762,8 @@ pub const Blob = struct { if (!strings.isAllASCII(slice)) { break :inner; } + blob.content_type_was_set = true; + if (globalThis.bunVM().mimeType(slice)) |mime| { blob.content_type = mime.value; break :inner; @@ -2769,6 +2779,7 @@ pub const Blob = struct { if (blob.content_type.len == 0) { blob.content_type = ""; + blob.content_type_was_set = false; } }, } @@ -2870,8 +2881,33 @@ pub const Blob = struct { /// This creates a new view /// and increment the reference count pub fn dupe(this: *const Blob) Blob { + return this.dupeWithContentType(false); + } + + pub fn dupeWithContentType(this: *const Blob, include_content_type: bool) Blob { if (this.store != null) this.store.?.ref(); var duped = this.*; + if (duped.content_type_allocated and duped.allocator != null and !include_content_type) { + + // for now, we just want to avoid a use-after-free here + if (JSC.VirtualMachine.get().mimeType(duped.content_type)) |mime| { + duped.content_type = mime.value; + } else { + // TODO: fix this + // this is a bug. + // it means whenever + duped.content_type = ""; + } + + duped.content_type_allocated = false; + duped.content_type_was_set = false; + if (this.content_type_was_set) { + duped.content_type_was_set = duped.content_type.len > 0; + } + } else if (duped.content_type_allocated and duped.allocator != null and include_content_type) { + duped.content_type = bun.default_allocator.dupe(u8, this.content_type) catch @panic("Out of memory"); + } + duped.allocator = null; return duped; } @@ -3477,6 +3513,13 @@ pub const AnyBlob = union(enum) { // InlineBlob: InlineBlob, InternalBlob: InternalBlob, + pub fn hasContentTypeFromUser(this: AnyBlob) bool { + return switch (this) { + .Blob => this.Blob.hasContentTypeFromUser(), + .InternalBlob => false, + }; + } + pub fn toJSON(this: *AnyBlob, global: *JSGlobalObject, comptime lifetime: JSC.WebCore.Lifetime) JSValue { switch (this.*) { .Blob => return this.Blob.toJSON(global, lifetime), diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 8d1bfb961..ad3857685 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -629,7 +629,7 @@ pub const Fetch = struct { result: HTTPClient.HTTPClientResult = .{}, javascript_vm: *VirtualMachine = undefined, global_this: *JSGlobalObject = undefined, - request_body: AnyBlob = undefined, + request_body: HTTPRequestBody = undefined, response_buffer: MutableString = undefined, request_headers: Headers = Headers{ .allocator = undefined }, promise: JSC.JSPromise.Strong, @@ -647,6 +647,38 @@ pub const Fetch = struct { abort_reason: JSValue = JSValue.zero, // Custom Hostname hostname: ?[]u8 = null, + + 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) + _ = JSC.Node.Syscall.close(this.Sendfile.fd); + this.Sendfile.offset = 0; + this.Sendfile.remain = 0; + }, + } + } + }; + pub fn init(_: std.mem.Allocator) anyerror!FetchTasklet { return FetchTasklet{}; } @@ -850,12 +882,26 @@ pub const Fetch = struct { proxy = jsc_vm.bundler.env.getHttpProxy(fetch_options.url); } - fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init(allocator, fetch_options.method, fetch_options.url, fetch_options.headers.entries, fetch_options.headers.buf.items, &fetch_tasklet.response_buffer, fetch_tasklet.request_body.slice(), fetch_options.timeout, HTTPClient.HTTPClientResult.Callback.New( - *FetchTasklet, - FetchTasklet.callback, - ).init( - fetch_tasklet, - ), proxy, if (fetch_tasklet.signal != null) &fetch_tasklet.aborted else null, fetch_options.hostname, fetch_options.redirect_type); + fetch_tasklet.http.?.* = HTTPClient.AsyncHTTP.init( + allocator, + fetch_options.method, + fetch_options.url, + fetch_options.headers.entries, + fetch_options.headers.buf.items, + &fetch_tasklet.response_buffer, + fetch_tasklet.request_body.slice(), + fetch_options.timeout, + HTTPClient.HTTPClientResult.Callback.New( + *FetchTasklet, + FetchTasklet.callback, + ).init( + fetch_tasklet, + ), + proxy, + if (fetch_tasklet.signal != null) &fetch_tasklet.aborted else null, + fetch_options.hostname, + fetch_options.redirect_type, + ); if (fetch_options.redirect_type != FetchRedirect.follow) { fetch_tasklet.http.?.client.remaining_redirect_count = 0; @@ -865,6 +911,12 @@ pub const Fetch = struct { fetch_tasklet.http.?.client.verbose = fetch_options.verbose; fetch_tasklet.http.?.client.disable_keepalive = fetch_options.disable_keepalive; + 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); } @@ -886,7 +938,7 @@ pub const Fetch = struct { const FetchOptions = struct { method: Method, headers: Headers, - body: AnyBlob, + body: HTTPRequestBody, timeout: usize, disable_timeout: bool, disable_keepalive: bool, @@ -961,6 +1013,14 @@ pub const Fetch = struct { 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 = .{}, }; @@ -988,46 +1048,45 @@ pub const Fetch = struct { 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| { + bun.default_allocator.free(host); + } + // 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| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator) catch unreachable; + headers = Headers.from(headers__, bun.default_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| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator) catch unreachable; + headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; headers__.deref(); } else if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(head, bun.default_allocator) catch unreachable; + headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; } } else if (request.headers) |head| { - headers = Headers.from(head, bun.default_allocator) catch unreachable; - } - - if (options.fastGet(ctx.ptr(), .body)) |body__| { - if (Body.Value.fromJS(ctx.ptr(), body__)) |body_const| { - var body_value = body_const; - // TODO: buffer ReadableStream? - // we have to explicitly check for InternalBlob - - body = body_value.useAsAnyBlob(); - } else { - // clean hostname if any - if (hostname) |host| { - bun.default_allocator.free(host); - } - // an error was thrown - return JSC.JSValue.jsUndefined(); - } - } else { - body = request.body.value.useAsAnyBlob(); + headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; } if (options.get(ctx, "timeout")) |timeout_value| { @@ -1100,13 +1159,13 @@ pub const Fetch = struct { } } else { method = request.method; + body = request.body.value.useAsAnyBlob(); if (request.headers) |head| { if (head.fastGet(JSC.FetchHeaders.HTTPHeaderName.Host)) |_hostname| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(head, bun.default_allocator) catch unreachable; + headers = Headers.from(head, bun.default_allocator, .{ .body = &body }) catch unreachable; } - body = request.body.value.useAsAnyBlob(); // no proxy only url url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable); url_proxy_buffer = url.href; @@ -1124,19 +1183,35 @@ pub const Fetch = struct { 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| { + bun.default_allocator.free(host); + } + // 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| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator) catch unreachable; + headers = Headers.from(headers__, bun.default_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| { hostname = _hostname.toOwnedSliceZ(bun.default_allocator) catch unreachable; } - headers = Headers.from(headers__, bun.default_allocator) catch unreachable; + headers = Headers.from(headers__, bun.default_allocator, .{ .body = &body }) catch unreachable; } else { // Converting the headers failed; return null and // let the set exception get thrown @@ -1144,22 +1219,6 @@ pub const Fetch = struct { } } - 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| { - bun.default_allocator.free(host); - } - // an error was thrown - return JSC.JSValue.jsUndefined(); - } - } - if (options.get(ctx, "timeout")) |timeout_value| { if (timeout_value.isBoolean()) { disable_timeout = !timeout_value.asBoolean(); @@ -1324,6 +1383,125 @@ pub const Fetch = struct { return JSPromise.rejectedPromiseValue(globalThis, err); } + if (headers == null and body.size() > 0 and body.hasContentTypeFromUser()) { + headers = Headers.from( + null, + bun.default_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| JSC.Node.Maybe(bun.FileDescriptor).errnoSysFd(JSC.Node.Syscall.system.dup(fd), .open, fd) orelse .{ .result = fd }, + .path => |path| JSC.Node.Syscall.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| { + bun.default_allocator.free(url_proxy_buffer); + + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + body.detach(); + if (headers) |*headers_| { + headers_.buf.deinit(bun.default_allocator); + headers_.entries.deinit(bun.default_allocator); + } + + return rejected_value; + }, + .result => |fd| fd, + }; + + if (proxy == null and bun.HTTP.Sendfile.isEligible(url)) { + use_sendfile: { + const stat: std.os.Stat = switch (JSC.Node.Syscall.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 (!std.os.S.ISREG(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 = @intCast(Blob.SizeType, stat.size); + const blob_size = if (std.os.S.ISREG(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 (std.os.S.ISREG(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) { + _ = JSC.Node.Syscall.close(opened_fd); + } + + switch (res) { + .err => |err| { + bun.default_allocator.free(url_proxy_buffer); + + const rejected_value = JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + body.detach(); + if (headers) |*headers_| { + headers_.buf.deinit(bun.default_allocator); + headers_.entries.deinit(bun.default_allocator); + } + + return rejected_value; + }, + .result => |result| { + body.detach(); + body.from(std.ArrayList(u8).fromOwnedSlice(bun.default_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); @@ -1340,7 +1518,7 @@ pub const Fetch = struct { .headers = headers orelse Headers{ .allocator = bun.default_allocator, }, - .body = body, + .body = http_body, .timeout = std.time.ns_per_hour, .disable_keepalive = disable_keepalive, .disable_timeout = disable_timeout, @@ -1376,15 +1554,31 @@ pub const Headers = struct { ""; } - pub fn from(headers_ref: *FetchHeaders, allocator: std.mem.Allocator) !Headers { + 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; - headers_ref.count(&header_count, &buf_len); + 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 += @truncate(u32, 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; @@ -1392,7 +1586,24 @@ pub const Headers = struct { var sliced = headers.entries.slice(); var names = sliced.items(.name); var values = sliced.items(.value); - headers_ref.copyTo(names.ptr, values.ptr, headers.buf.items.ptr); + 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 = @truncate(u32, options.body.?.contentType().len), + }; + } + return headers; } }; @@ -1567,7 +1778,7 @@ pub const FetchEvent = struct { var content_length: ?usize = null; if (response.body.init.headers) |headers_ref| { - var headers = Headers.from(headers_ref, request_context.allocator) catch unreachable; + var headers = Headers.from(headers_ref, request_context.allocator, .{}) catch unreachable; var i: usize = 0; while (i < headers.entries.len) : (i += 1) { |