diff options
author | 2023-10-16 15:57:16 -0700 | |
---|---|---|
committer | 2023-10-16 15:57:16 -0700 | |
commit | c5354951bacb9efee26683d1be83177443e0d784 (patch) | |
tree | a2fcb4ca40ac2e3db359295b867d5da2175c6be4 | |
parent | f1658e2e58acb1fcdb3181e8238c50b961b822f6 (diff) | |
download | bun-c5354951bacb9efee26683d1be83177443e0d784.tar.gz bun-c5354951bacb9efee26683d1be83177443e0d784.tar.zst bun-c5354951bacb9efee26683d1be83177443e0d784.zip |
Fix `Response.statusText` (#6151)
-rw-r--r-- | src/bun.js/api/html_rewriter.zig | 28 | ||||
-rw-r--r-- | src/bun.js/api/server.zig | 6 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 4 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 3 | ||||
-rw-r--r-- | src/bun.js/webcore/body.zig | 156 | ||||
-rw-r--r-- | src/bun.js/webcore/request.zig | 7 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 262 | ||||
-rw-r--r-- | src/js/builtins/BunBuiltinNames.h | 1 | ||||
-rw-r--r-- | test/js/bun/util/inspect.test.js | 8 | ||||
-rw-r--r-- | test/js/web/fetch/response.test.ts | 32 |
10 files changed, 269 insertions, 238 deletions
diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index 1bda47512..1f2366ad9 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -384,10 +384,10 @@ pub const HTMLRewriter = struct { result.* = Response{ .allocator = bun.default_allocator, + .init = .{ + .status_code = 200, + }, .body = .{ - .init = .{ - .status_code = 200, - }, .value = .{ .Locked = .{ .global = global, @@ -397,16 +397,16 @@ pub const HTMLRewriter = struct { }, }; - result.body.init.method = original.body.init.method; - result.body.init.status_code = original.body.init.status_code; + result.init.method = original.init.method; + result.init.status_code = original.init.status_code; + result.init.status_text = original.init.status_text.clone(); // https://github.com/oven-sh/bun/issues/3334 - if (original.body.init.headers) |headers| { - result.body.init.headers = headers.cloneThis(global); + if (original.init.headers) |headers| { + result.init.headers = headers.cloneThis(global); } result.url = original.url.clone(); - result.status_text = original.status_text.clone(); var value = original.getBodyValue(); sink.bodyValueBufferer = JSC.WebCore.BodyValueBufferer.init(sink, onFinishedBuffering, sink.global, bun.default_allocator); sink.bodyValueBufferer.?.run(value) catch |buffering_error| { @@ -606,10 +606,10 @@ pub const HTMLRewriter = struct { // result.* = Response{ // .allocator = bun.default_allocator, + // .init = .{ + // .status_code = 200, + // }, // .body = .{ - // .init = .{ - // .status_code = 200, - // }, // .value = .{ // .Locked = .{ // .global = global, @@ -619,9 +619,9 @@ pub const HTMLRewriter = struct { // }, // }; - // result.body.init.headers = original.body.init.headers; - // result.body.init.method = original.body.init.method; - // result.body.init.status_code = original.body.init.status_code; + // result.init.headers = original.init.headers; + // result.init.method = original.init.method; + // result.init.status_code = original.init.status_code; // result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable; // result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index d90c22577..1df908a0b 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2988,7 +2988,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp var needs_content_type = true; const content_type: MimeType = brk: { - if (response.body.init.headers) |headers_| { + if (response.init.headers) |headers_| { if (headers_.fastGet(.ContentType)) |content| { needs_content_type = false; break :brk MimeType.byName(content.slice()); @@ -3008,7 +3008,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }; var has_content_disposition = false; - if (response.body.init.headers) |headers_| { + if (response.init.headers) |headers_| { has_content_disposition = headers_.fastHas(.ContentDisposition); needs_content_range = needs_content_range and headers_.fastHas(.ContentRange); if (needs_content_range) { @@ -3018,7 +3018,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.writeStatus(status); this.writeHeaders(headers_); - response.body.init.headers = null; + response.init.headers = null; headers_.deref(); } else if (needs_content_range) { status = 206; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 778ce4bce..49300198e 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4100,6 +4100,7 @@ enum class BuiltinNamesMap : uint8_t { method, headers, status, + statusText, url, body, data, @@ -4119,6 +4120,9 @@ static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigne case BuiltinNamesMap::headers: { return clientData->builtinNames().headersPublicName(); } + case BuiltinNamesMap::statusText: { + return clientData->builtinNames().statusTextPublicName(); + } case BuiltinNamesMap::status: { return clientData->builtinNames().statusPublicName(); } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index d11017438..1ab1ca603 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4265,6 +4265,7 @@ pub const JSValue = enum(JSValueReprInt) { method, headers, status, + statusText, url, body, data, @@ -5833,7 +5834,7 @@ pub fn initialize() void { \\ \\ https://github.com/oven-sh/webkit/blob/main/Source/JavaScriptCore/runtime/OptionsList.h \\ - \\Environment variables must be prefixed with "BUN_JSC_". This code runs before .env files are loaded, so those won't work here. + \\Environment variables must be prefixed with "BUN_JSC_". This code runs before .env files are loaded, so those won't work here. \\ \\Warning: options change between releases of Bun and WebKit without notice. This is not a stable API, you should not rely on it beyond debugging something, and it may be removed entirely in a future version of Bun. , diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 76cda1bad..e38f03c08 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -51,7 +51,6 @@ const Request = JSC.WebCore.Request; // https://developer.mozilla.org/en-US/docs/Web/API/Body pub const Body = struct { - init: Init = Init{ .headers = null, .status_code = 200 }, value: Value, // = Value.empty, pub inline fn len(this: *const Body) Blob.SizeType { @@ -68,7 +67,6 @@ pub const Body = struct { pub fn clone(this: *Body, globalThis: *JSGlobalObject) Body { return Body{ - .init = this.init.clone(globalThis), .value = this.value.clone(globalThis), }; } @@ -79,19 +77,7 @@ pub const Body = struct { try formatter.writeIndent(Writer, writer); try writer.writeAll(comptime Output.prettyFmt("<r>bodyUsed<d>:<r> ", enable_ansi_colors)); formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors); - formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable; - try writer.writeAll("\n"); - // if (this.init.headers) |headers| { - // try formatter.writeIndent(Writer, writer); - // try writer.writeAll("headers: "); - // try headers.leak().writeFormat(formatter, writer, comptime enable_ansi_colors); - // try writer.writeAll("\n"); - // } - - try formatter.writeIndent(Writer, writer); - try writer.writeAll(comptime Output.prettyFmt("<r>status<d>:<r> ", enable_ansi_colors)); - formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors); if (this.value == .Blob) { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); @@ -113,87 +99,9 @@ pub const Body = struct { } pub fn deinit(this: *Body, _: std.mem.Allocator) void { - if (this.init.headers) |headers| { - this.init.headers = null; - - headers.deref(); - } this.value.deinit(); } - pub const Init = struct { - headers: ?*FetchHeaders = null, - status_code: u16, - method: Method = Method.GET, - - pub fn clone(this: Init, ctx: *JSGlobalObject) Init { - var that = this; - var headers = this.headers; - if (headers) |head| { - that.headers = head.cloneThis(ctx); - } - - return that; - } - - pub fn init(allocator: std.mem.Allocator, ctx: *JSGlobalObject, response_init: JSC.JSValue) !?Init { - var result = Init{ .status_code = 200 }; - - if (!response_init.isCell()) - return null; - - if (response_init.jsType() == .DOMWrapper) { - // fast path: it's a Request object or a Response object - // we can skip calling JS getters - if (response_init.as(Request)) |req| { - if (req.headers) |headers| { - if (!headers.isEmpty()) { - result.headers = headers.cloneThis(ctx); - } - } - - result.method = req.method; - return result; - } - - if (response_init.as(Response)) |req| { - return req.body.init.clone(ctx); - } - } - - if (response_init.fastGet(ctx, .headers)) |headers| { - if (headers.as(FetchHeaders)) |orig| { - if (!orig.isEmpty()) { - result.headers = orig.cloneThis(ctx); - } - } else { - result.headers = FetchHeaders.createFromJS(ctx.ptr(), headers); - } - } - - if (response_init.fastGet(ctx, .status)) |status_value| { - const number = status_value.coerceToInt64(ctx); - if ((200 <= number and number < 600) or number == 101) { - result.status_code = @as(u16, @truncate(@as(u32, @intCast(number)))); - } else { - const err = ctx.createRangeErrorInstance("The status provided ({d}) must be 101 or in the range of [200, 599]", .{number}); - ctx.throwValue(err); - return null; - } - } - - if (response_init.fastGet(ctx, .method)) |method_value| { - var method_str = method_value.toSlice(ctx, allocator); - defer method_str.deinit(); - if (method_str.len > 0) { - result.method = Method.which(method_str.slice()) orelse .GET; - } - } - - return result; - } - }; - pub const PendingValue = struct { promise: ?JSValue = null, readable: ?JSC.WebCore.ReadableStream = null, @@ -1004,72 +912,12 @@ pub const Body = struct { } }; - pub fn @"404"(_: js.JSContextRef) Body { - return Body{ - .init = Init{ - .headers = null, - .status_code = 404, - }, - .value = Value{ .Null = {} }, - }; - } - - pub fn @"200"(_: js.JSContextRef) Body { - return Body{ - .init = Init{ - .status_code = 200, - }, - .value = Value{ .Null = {} }, - }; - } - - pub fn extract( - globalThis: *JSGlobalObject, - value: JSValue, - ) ?Body { - return extractBody( - globalThis, - value, - false, - JSValue.zero, - ); - } - - pub fn extractWithInit( - globalThis: *JSGlobalObject, - value: JSValue, - init: JSValue, - ) ?Body { - return extractBody( - globalThis, - value, - true, - init, - ); - } - // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45 - inline fn extractBody( + pub fn extract( globalThis: *JSGlobalObject, value: JSValue, - comptime has_init: bool, - init: JSValue, ) ?Body { - var body = Body{ - .value = Value{ .Null = {} }, - .init = Init{ .headers = null, .status_code = 200 }, - }; - var allocator = getAllocator(globalThis); - - if (comptime has_init) { - if (Init.init(allocator, globalThis, init)) |maybeInit| { - if (maybeInit) |init_| { - body.init = init_; - } - } else |_| { - return null; - } - } + var body = Body{ .value = Value{ .Null = {} } }; body.value = Value.fromJS(globalThis, value) orelse return null; if (body.value == .Blob) diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 7f37bea46..95f92145b 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -50,6 +50,7 @@ const InternalBlob = JSC.WebCore.InternalBlob; const BodyMixin = JSC.WebCore.BodyMixin; const Body = JSC.WebCore.Body; const Blob = JSC.WebCore.Blob; +const Response = JSC.WebCore.Response; const body_value_pool_size: u16 = 256; pub const BodyValueRef = bun.HiveRef(Body.Value, body_value_pool_size); @@ -542,12 +543,12 @@ pub const Request = struct { if (value.as(JSC.WebCore.Response)) |response| { if (!fields.contains(.method)) { - req.method = response.body.init.method; + req.method = response.init.method; fields.insert(.method); } if (!fields.contains(.headers)) { - if (response.body.init.headers) |headers| { + if (response.init.headers) |headers| { req.headers = headers.cloneThis(globalThis); fields.insert(.headers); } @@ -623,7 +624,7 @@ pub const Request = struct { } if (!fields.contains(.method) or !fields.contains(.headers)) { - if (Body.Init.init(globalThis.allocator(), globalThis, value) catch null) |init| { + if (Response.Init.init(globalThis.allocator(), globalThis, value) catch null) |init| { if (!explicit_check or (explicit_check and value.fastGet(globalThis, .method) != null)) { if (!fields.contains(.method)) { req.method = init.method; diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 9a2ec48ba..f7ada2862 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -63,8 +63,8 @@ pub const Response = struct { allocator: std.mem.Allocator, body: Body, + init: Init, url: bun.String = bun.String.empty, - status_text: bun.String = bun.String.empty, redirected: bool = false, // We must report a consistent value for this @@ -89,7 +89,7 @@ pub const Response = struct { 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)), + @intCast(this.body.value.estimatedSize() + this.url.byteSlice().len + this.init.status_text.byteSlice().len + @sizeOf(Response)), ); break :brk this.reported_estimated_size.?; }; @@ -104,11 +104,11 @@ pub const Response = struct { pub fn getFetchHeaders( this: *Response, ) ?*FetchHeaders { - return this.body.init.headers; + return this.init.headers; } pub inline fn statusCode(this: *const Response) u16 { - return this.body.init.status_code; + return this.init.status_code; } pub fn redirectLocation(this: *const Response) ?[]const u8 { @@ -116,7 +116,7 @@ pub const Response = struct { } pub fn header(this: *const Response, name: JSC.FetchHeaders.HTTPHeaderName) ?[]const u8 { - return if ((this.body.init.headers orelse return null).fastGet(name)) |str| + return if ((this.init.headers orelse return null).fastGet(name)) |str| str.slice() else null; @@ -146,14 +146,20 @@ pub const Response = struct { 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); + try writer.writeAll(comptime Output.prettyFmt("<r>status<d>:<r> ", enable_ansi_colors)); + formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, 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}); + try writer.print(comptime Output.prettyFmt("<r>\"<b>{}<r>\"", enable_ansi_colors), .{this.init.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>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"); @@ -162,6 +168,7 @@ pub const Response = struct { 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); } @@ -172,7 +179,7 @@ pub const Response = struct { } 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); + return this.init.status_code >= 200 and this.init.status_code <= 299; } pub fn getURL( @@ -187,7 +194,7 @@ pub const Response = struct { this: *Response, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { - if (this.body.init.status_code < 200) { + if (this.init.status_code < 200) { return ZigString.init("error").toValue(globalThis); } @@ -199,7 +206,7 @@ pub const Response = struct { globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSC.JSValue { // https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText - return this.status_text.toJS(globalThis); + return this.init.status_text.toJS(globalThis); } pub fn getRedirected( @@ -219,18 +226,18 @@ pub const Response = struct { } fn getOrCreateHeaders(this: *Response, globalThis: *JSC.JSGlobalObject) *FetchHeaders { - if (this.body.init.headers == null) { - this.body.init.headers = FetchHeaders.createEmpty(); + if (this.init.headers == null) { + this.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); + this.init.headers.?.put("content-type", content_type, globalThis); } } } - return this.body.init.headers.?; + return this.init.headers.?; } pub fn getHeaders( @@ -262,8 +269,8 @@ pub const Response = struct { new_response.* = Response{ .allocator = allocator, .body = this.body.clone(globalThis), + .init = this.init.clone(globalThis), .url = this.url.clone(), - .status_text = this.status_text.clone(), .redirected = this.redirected, }; } @@ -279,17 +286,16 @@ pub const Response = struct { _: *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); + return JSValue.jsNumber(this.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.init.deinit(allocator); + this.body.deinit(allocator); this.url.deref(); allocator.destroy(this); @@ -348,7 +354,7 @@ pub const Response = struct { pub fn getContentType( this: *Response, ) ?ZigString.Slice { - if (this.body.init.headers) |headers| { + if (this.init.headers) |headers| { if (headers.fastGet(.ContentType)) |value| { return value.toSlice(bun.default_allocator); } @@ -373,11 +379,11 @@ pub const Response = struct { var response = Response{ .body = Body{ - .init = Body.Init{ - .status_code = 200, - }, .value = .{ .Empty = {} }, }, + .init = Response.Init{ + .status_code = 200, + }, .allocator = getAllocator(globalThis), .url = bun.String.empty, }; @@ -409,10 +415,10 @@ pub const Response = struct { 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)))); + response.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; + if (Response.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| { + response.init = _init; } } } @@ -434,10 +440,10 @@ pub const Response = struct { // var response = getAllocator(globalThis).create(Response) catch unreachable; var response = Response{ + .init = Response.Init{ + .status_code = 302, + }, .body = Body{ - .init = Body.Init{ - .status_code = 302, - }, .value = .{ .Empty = {} }, }, .allocator = getAllocator(globalThis), @@ -455,17 +461,17 @@ pub const Response = struct { 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)))); + response.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; + if (Response.Init.init(getAllocator(globalThis), globalThis, init) catch null) |_init| { + response.init = _init; + response.init.status_code = 302; } } } - response.body.init.headers = response.getOrCreateHeaders(globalThis); - var headers_ref = response.body.init.headers.?; + response.init.headers = response.getOrCreateHeaders(globalThis); + var headers_ref = response.init.headers.?; headers_ref.put("location", url_string_slice.slice(), globalThis); var ptr = response.allocator.create(Response) catch unreachable; ptr.* = response; @@ -478,10 +484,10 @@ pub const Response = struct { ) callconv(.C) JSValue { var response = getAllocator(globalThis).create(Response) catch unreachable; response.* = Response{ + .init = Response.Init{ + .status_code = 0, + }, .body = Body{ - .init = Body.Init{ - .status_code = 0, - }, .value = .{ .Empty = {} }, }, .allocator = getAllocator(globalThis), @@ -494,6 +500,8 @@ pub const Response = struct { globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, ) callconv(.C) ?*Response { + var allocator = getAllocator(globalThis); + const args_list = brk: { var args = callframe.arguments(2); if (args.len > 1 and args.ptr[1].isEmptyOrUndefinedOrNull()) { @@ -503,17 +511,24 @@ pub const Response = struct { }; const arguments = args_list.ptr[0..args_list.len]; - const body: Body = @as(?Body, brk: { + + const init: Init = @as(?Init, brk: { switch (arguments.len) { 0 => { - break :brk Body.@"200"(globalThis); + break :brk Init{ + .status_code = 200, + .headers = null, + }; }, 1 => { - break :brk Body.extract(globalThis, arguments[0]); + break :brk Init{ + .status_code = 200, + .headers = null, + }; }, else => { if (arguments[1].isObject()) { - break :brk Body.extractWithInit(globalThis, arguments[0], arguments[1]); + break :brk Init.init(allocator, globalThis, arguments[1]) catch null; } std.debug.assert(!arguments[1].isEmptyOrUndefinedOrNull()); @@ -526,23 +541,152 @@ pub const Response = struct { unreachable; }) orelse return null; - var response = getAllocator(globalThis).create(Response) catch unreachable; + const body: Body = brk: { + switch (arguments.len) { + 0 => { + break :brk Body{ + .value = Body.Value{ .Null = {} }, + }; + }, + else => { + break :brk Body.extract(globalThis, arguments[0]); + }, + } + unreachable; + } orelse return null; + + var response = allocator.create(Response) catch unreachable; response.* = Response{ .body = body, + .init = init, .allocator = getAllocator(globalThis), }; if (response.body.value == .Blob and - response.body.init.headers != null and + response.init.headers != null and response.body.value.Blob.content_type.len > 0 and - !response.body.init.headers.?.fastHas(.ContentType)) + !response.init.headers.?.fastHas(.ContentType)) { - response.body.init.headers.?.put("content-type", response.body.value.Blob.content_type, globalThis); + response.init.headers.?.put("content-type", response.body.value.Blob.content_type, globalThis); } return response; } + + pub const Init = struct { + headers: ?*FetchHeaders = null, + status_code: u16, + status_text: bun.String = bun.String.empty, + method: Method = Method.GET, + + pub fn clone(this: Init, ctx: *JSGlobalObject) Init { + var that = this; + var headers = this.headers; + if (headers) |head| { + that.headers = head.cloneThis(ctx); + } + that.status_text = this.status_text.clone(); + + return that; + } + + pub fn init(allocator: std.mem.Allocator, ctx: *JSGlobalObject, response_init: JSC.JSValue) !?Init { + var result = Init{ .status_code = 200 }; + + if (!response_init.isCell()) + return null; + + if (response_init.jsType() == .DOMWrapper) { + // fast path: it's a Request object or a Response object + // we can skip calling JS getters + if (response_init.as(Request)) |req| { + if (req.headers) |headers| { + if (!headers.isEmpty()) { + result.headers = headers.cloneThis(ctx); + } + } + + result.method = req.method; + return result; + } + + if (response_init.as(Response)) |resp| { + return resp.init.clone(ctx); + } + } + + if (response_init.fastGet(ctx, .headers)) |headers| { + if (headers.as(FetchHeaders)) |orig| { + if (!orig.isEmpty()) { + result.headers = orig.cloneThis(ctx); + } + } else { + result.headers = FetchHeaders.createFromJS(ctx.ptr(), headers); + } + } + + if (response_init.fastGet(ctx, .status)) |status_value| { + const number = status_value.coerceToInt64(ctx); + if ((200 <= number and number < 600) or number == 101) { + result.status_code = @as(u16, @truncate(@as(u32, @intCast(number)))); + } else { + const err = ctx.createRangeErrorInstance("The status provided ({d}) must be 101 or in the range of [200, 599]", .{number}); + ctx.throwValue(err); + return null; + } + } + + if (response_init.fastGet(ctx, .statusText)) |status_text| { + result.status_text = bun.String.fromJS(status_text, ctx).dupeRef(); + } + + if (response_init.fastGet(ctx, .method)) |method_value| { + var method_str = method_value.toSlice(ctx, allocator); + defer method_str.deinit(); + if (method_str.len > 0) { + result.method = Method.which(method_str.slice()) orelse .GET; + } + } + + return result; + } + + pub fn deinit(this: *Init, _: std.mem.Allocator) void { + if (this.headers) |headers| { + this.headers = null; + + headers.deref(); + } + + this.status_text.deref(); + } + }; + + pub fn @"404"(globalThis: *JSC.JSGlobalObject) Response { + return emptyWithStatus(globalThis, 404); + } + + pub fn @"200"(globalThis: *JSC.JSGlobalObject) Response { + return emptyWithStatus(globalThis, 200); + } + + inline fn emptyWithStatus(globalThis: *JSC.JSGlobalObject, status: u16) Response { + var allocator = getAllocator(globalThis); + var response = allocator.create(Response) catch unreachable; + + response.* = Response{ + .body = Body{ + .value = Body.Value{ .Null = {} }, + }, + .init = Init{ + .status_code = status, + }, + .allocator = getAllocator(globalThis), + }; + + return response; + } }; const null_fd = bun.invalid_fd; @@ -1238,13 +1382,13 @@ pub const Fetch = struct { return Response{ .allocator = allocator, .url = bun.String.createAtomIfPossible(metadata.url), - .status_text = bun.String.createAtomIfPossible(http_response.status), .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 = .{ - .init = .{ - .headers = FetchHeaders.createFromPicoHeaders(http_response.headers), - .status_code = @as(u16, @truncate(http_response.status_code)), - }, .value = this.toBodyValue(), }, }; @@ -1488,15 +1632,15 @@ pub const Fetch = struct { response.* = Response{ .body = Body{ - .init = Body.Init{ - .status_code = 200, - }, .value = .{ .Blob = blob, }, }, + .init = Response.Init{ + .status_code = 200, + .status_text = bun.String.createAtom("OK"), + }, .allocator = allocator, - .status_text = bun.String.createAtom("OK"), .url = data_url.url.dupeRef(), }; @@ -2009,11 +2153,11 @@ pub const Fetch = struct { response.* = Response{ .body = Body{ - .init = Body.Init{ - .status_code = 200, - }, .value = .{ .Blob = bun_file }, }, + .init = Response.Init{ + .status_code = 200, + }, .allocator = bun.default_allocator, .url = file_url_string.clone(), }; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 3b29b75b2..651cac59b 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -210,6 +210,7 @@ using namespace JSC; macro(startedPromise) \ macro(state) \ macro(status) \ + macro(statusText) \ macro(storedError) \ macro(strategy) \ macro(strategyHWM) \ diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 32b3f2bfa..c2cfe11bf 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -55,21 +55,21 @@ it("Blob inspect", () => { expect(Bun.inspect(new Response(new Blob()))).toBe(`Response (0 KB) { ok: true, url: "", - headers: Headers {}, + status: 200, statusText: "", + headers: Headers {}, redirected: false, bodyUsed: false, - status: 200, [Blob detached] }`); expect(Bun.inspect(new Response("Hello"))).toBe(`Response (5 bytes) { ok: true, url: "", - headers: Headers {}, + status: 200, statusText: "", + headers: Headers {}, redirected: false, bodyUsed: false, - status: 200, Blob (5 bytes) }`); }); diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts new file mode 100644 index 000000000..ddfcd70f7 --- /dev/null +++ b/test/js/web/fetch/response.test.ts @@ -0,0 +1,32 @@ +import { describe, test, expect } from "bun:test"; + +test("zero args returns an otherwise empty 200 response", () => { + const response = new Response(); + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +test("1-arg form returns a 200 response", () => { + const response = new Response("body text"); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +describe("2-arg form", () => { + test("can fill in status/statusText, and it works", () => { + const response = new Response("body text", { + status: 202, + statusText: "Accepted.", + }); + + expect(response.status).toBe(202); + expect(response.statusText).toBe("Accepted."); + }); + test('empty object continues to return 200/""', () => { + const response = new Response("body text", {}); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); + }); +}); |