Files
bun.sh/src/bun.js/webcore/Response.zig
2025-09-04 14:09:34 -07:00

921 lines
32 KiB
Zig

const Response = @This();
// C++ helper functions for AsyncLocalStorage integration
extern fn Response__getAsyncLocalStorageStore(global: *JSGlobalObject, als: JSValue) JSValue;
extern fn Response__mergeAsyncLocalStorageOptions(global: *JSGlobalObject, alsStore: JSValue, initOptions: JSValue) void;
// Zig function to update AsyncLocalStorage with response options
pub fn bakeGetAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const vm = global.bunVM();
// Get the AsyncLocalStorage instance from the VM
if (vm.getDevServerAsyncLocalStorage()) |als| {
return als;
}
return .js_undefined;
}
const ResponseMixin = BodyMixin(@This());
pub const js = jsc.Codegen.JSResponse;
// NOTE: toJS is overridden
pub const fromJS = js.fromJS;
pub const fromJSDirect = js.fromJSDirect;
body: Body,
init: Init,
url: bun.String = bun.String.empty,
redirected: bool = false,
/// We increment this count in fetch so if JS Response is discarted we can resolve the Body
/// In the server we use a flag response_protected to protect/unprotect the response
ref_count: u32 = 1,
// We must report a consistent value for this
reported_estimated_size: usize = 0,
pub const getText = ResponseMixin.getText;
pub const getBody = ResponseMixin.getBody;
pub const getBytes = ResponseMixin.getBytes;
pub const getBodyUsed = ResponseMixin.getBodyUsed;
pub const getJSON = ResponseMixin.getJSON;
pub const getArrayBuffer = ResponseMixin.getArrayBuffer;
pub const getBlob = ResponseMixin.getBlob;
pub const getBlobWithoutCallFrame = ResponseMixin.getBlobWithoutCallFrame;
pub const getFormData = ResponseMixin.getFormData;
pub fn getFormDataEncoding(this: *Response) bun.JSError!?*bun.FormData.AsyncFormData {
var content_type_slice: ZigString.Slice = (try 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(bun.default_allocator, encoding) catch bun.outOfMemory();
}
pub fn estimatedSize(this: *Response) callconv(.C) usize {
return this.reported_estimated_size;
}
pub fn calculateEstimatedByteSize(this: *Response) void {
this.reported_estimated_size = this.body.value.estimatedSize() +
this.url.byteSlice().len +
this.init.status_text.byteSlice().len +
@sizeOf(Response);
}
pub fn toJS(this: *Response, globalObject: *JSGlobalObject) JSValue {
this.calculateEstimatedByteSize();
return js.toJSUnchecked(globalObject, this);
}
pub fn getBodyValue(
this: *Response,
) *Body.Value {
return &this.body.value;
}
pub export fn jsFunctionRequestOrResponseHasBodyValue(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
_ = globalObject; // autofix
const arguments = callframe.arguments_old(1);
const this_value = arguments.ptr[0];
if (this_value.isEmptyOrUndefinedOrNull()) {
return .false;
}
if (this_value.as(Response)) |response| {
return jsc.JSValue.jsBoolean(!response.body.value.isDefinitelyEmpty());
} else if (this_value.as(Request)) |request| {
return jsc.JSValue.jsBoolean(!request.body.value.isDefinitelyEmpty());
}
return .false;
}
pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {
const arguments = callframe.arguments_old(1);
const this_value = arguments.ptr[0];
if (this_value.isEmptyOrUndefinedOrNull()) {
return .js_undefined;
}
const body: *Body.Value = brk: {
if (this_value.as(Response)) |response| {
break :brk &response.body.value;
} else if (this_value.as(Request)) |request| {
break :brk &request.body.value;
}
return .js_undefined;
};
// Get the body if it's available synchronously.
switch (body.*) {
.Used, .Empty, .Null => return .js_undefined,
.Blob => |*blob| {
if (blob.isBunFile()) {
return .js_undefined;
}
defer body.* = .{ .Used = {} };
return blob.toArrayBuffer(globalObject, .transfer) catch return .zero;
},
.WTFStringImpl, .InternalBlob => {
var any_blob = body.useAsAnyBlob();
return any_blob.toArrayBufferTransfer(globalObject) catch return .zero;
},
.Error, .Locked, .Render => return .js_undefined,
}
}
pub fn getFetchHeaders(
this: *Response,
) ?*FetchHeaders {
return this.init.headers;
}
pub inline fn statusCode(this: *const Response) u16 {
return this.init.status_code;
}
pub fn redirectLocation(this: *const Response) ?[]const u8 {
return this.header(.Location);
}
pub fn header(this: *const Response, name: bun.webcore.FetchHeaders.HTTPHeaderName) ?[]const u8 {
return if (try (this.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));
try formatter.printAs(.Boolean, Writer, writer, jsc.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory();
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 bun.outOfMemory();
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>status<d>:<r> ", enable_ansi_colors));
try formatter.printAs(.Double, Writer, writer, jsc.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory();
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.init.status_text});
formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory();
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>headers<d>:<r> ", enable_ansi_colors));
try formatter.printAs(.Private, Writer, writer, try this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory();
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>redirected<d>:<r> ", enable_ansi_colors));
try formatter.printAs(.Boolean, Writer, writer, jsc.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch bun.outOfMemory();
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.init.status_code >= 200 and this.init.status_code <= 299;
}
pub fn getURL(
this: *Response,
globalThis: *jsc.JSGlobalObject,
) 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,
) jsc.JSValue {
if (this.init.status_code < 200) {
return bun.String.static("error").toJS(globalThis);
}
return bun.String.static("default").toJS(globalThis);
}
pub fn getStatusText(
this: *Response,
globalThis: *jsc.JSGlobalObject,
) jsc.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
return this.init.status_text.toJS(globalThis);
}
pub fn getRedirected(
this: *Response,
_: *jsc.JSGlobalObject,
) 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,
) 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) bun.JSError!*FetchHeaders {
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) {
try this.init.headers.?.put(.ContentType, content_type, globalThis);
}
}
}
return this.init.headers.?;
}
pub fn getHeaders(
this: *Response,
globalThis: *jsc.JSGlobalObject,
) bun.JSError!jsc.JSValue {
return (try this.getOrCreateHeaders(globalThis)).toJS(globalThis);
}
pub fn doClone(
this: *Response,
globalThis: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!JSValue {
const this_value = callframe.this();
const cloned = try this.clone(globalThis);
const js_wrapper = Response.makeMaybePooled(globalThis, cloned);
if (js_wrapper != .zero) {
if (cloned.body.value == .Locked) {
if (cloned.body.value.Locked.readable.get(globalThis)) |readable| {
// If we are teed, then we need to update the cached .body
// value to point to the new readable stream
// We must do this on both the original and cloned response
// but especially the original response since it will have a stale .body value now.
js.bodySetCached(js_wrapper, globalThis, readable.value);
if (this.body.value.Locked.readable.get(globalThis)) |other_readable| {
js.bodySetCached(this_value, globalThis, other_readable.value);
}
}
}
}
return js_wrapper;
}
pub fn makeMaybePooled(globalObject: *jsc.JSGlobalObject, ptr: *Response) JSValue {
return ptr.toJS(globalObject);
}
pub fn cloneValue(
this: *Response,
globalThis: *JSGlobalObject,
) bun.JSError!Response {
var body = try this.body.clone(globalThis);
errdefer body.deinit(bun.default_allocator);
var init = try this.init.clone(globalThis);
errdefer init.deinit(bun.default_allocator);
return Response{
.body = body,
.init = init,
.url = this.url.clone(),
.redirected = this.redirected,
};
}
pub fn clone(this: *Response, globalThis: *JSGlobalObject) bun.JSError!*Response {
// TODO: handle clone for jsxElement for bake?
return bun.new(Response, try this.cloneValue(globalThis));
}
pub fn getStatus(
this: *Response,
_: *jsc.JSGlobalObject,
) jsc.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/status
return JSValue.jsNumber(this.init.status_code);
}
fn destroy(this: *Response) void {
this.init.deinit(bun.default_allocator);
this.body.deinit(bun.default_allocator);
this.url.deref();
bun.destroy(this);
}
pub fn ref(this: *Response) *Response {
this.ref_count += 1;
return this;
}
pub fn unref(this: *Response) void {
bun.assert(this.ref_count > 0);
this.ref_count -= 1;
if (this.ref_count == 0) {
this.destroy();
}
}
pub fn finalize(
this: *Response,
) callconv(.C) void {
this.unref();
}
pub fn getContentType(
this: *Response,
) bun.JSError!?ZigString.Slice {
if (this.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,
) bun.JSError!JSValue {
const args_list = callframe.arguments_old(2);
// https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]);
var response = Response{
.body = Body{
.value = .{ .Empty = {} },
},
.init = Response.Init{
.status_code = 200,
},
.url = bun.String.empty,
};
var did_succeed = false;
defer {
if (!did_succeed) {
response.body.deinit(bun.default_allocator);
response.init.deinit(bun.default_allocator);
}
}
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
try json_value.jsonStringify(globalThis, 0, &str);
if (globalThis.hasException()) {
return .zero;
}
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.init.status_code = @as(u16, @intCast(@min(@max(0, init.toInt32()), std.math.maxInt(u16))));
} else {
if (Response.Init.init(globalThis, init) catch |err| if (err == error.JSError) return .zero else null) |_init| {
response.init = _init;
}
}
}
var headers_ref = try response.getOrCreateHeaders(globalThis);
try headers_ref.putDefault(.ContentType, MimeType.json.value, globalThis);
did_succeed = true;
return bun.new(Response, response).toJS(globalThis);
}
fn validateRedirectStatusCode(globalThis: *jsc.JSGlobalObject, status_code: i32) bun.JSError!u16 {
switch (status_code) {
301, 302, 303, 307, 308 => return @intCast(status_code),
else => {
const err = globalThis.createRangeErrorInstance("Failed to execute 'redirect' on 'Response': Invalid status code", .{});
return globalThis.throwValue(err);
},
}
}
pub fn constructRedirect(
globalThis: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!JSValue {
var args_list = callframe.arguments_old(4);
// https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4
var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), args_list.ptr[0..args_list.len]);
var url_string_slice = ZigString.Slice.empty;
defer url_string_slice.deinit();
var response: Response = brk: {
var response = Response{
.init = Response.Init{
.status_code = 302,
},
.body = Body{
.value = .{ .Empty = {} },
},
.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 = try url_string_value.getZigString(globalThis);
}
url_string_slice = url_string.toSlice(bun.default_allocator);
var did_succeed = false;
defer {
if (!did_succeed) {
response.body.deinit(bun.default_allocator);
response.init.deinit(bun.default_allocator);
}
}
if (args.nextEat()) |init| {
if (init.isUndefinedOrNull()) {} else if (init.isNumber()) {
response.init.status_code = try validateRedirectStatusCode(globalThis, init.toInt32());
} else if (try Response.Init.init(globalThis, init)) |_init| {
errdefer response.init.deinit(bun.default_allocator);
response.init = _init;
if (_init.status_code != 200) {
response.init.status_code = try validateRedirectStatusCode(globalThis, _init.status_code);
}
}
}
if (globalThis.hasException()) {
return .zero;
}
did_succeed = true;
break :brk response;
};
response.init.headers = try response.getOrCreateHeaders(globalThis);
var headers_ref = response.init.headers.?;
try headers_ref.put(.Location, url_string_slice.slice(), globalThis);
const ptr = bun.new(Response, response);
const response_js = ptr.toJS(globalThis);
// Check if dev_server_async_local_storage is set (indicating we're in Bun dev server)
const vm = globalThis.bunVM();
if (vm.dev_server_async_local_storage.get()) |async_local_storage| {
// Mark this as a redirect Response that should be handled specially
// when used in a React component
const redirect_marker = ZigString.init("__bun_redirect__").toJS(globalThis);
// Transform the Response to act as a React element with special redirect handling
// Pass "redirect" as the third parameter to indicate this is a redirect
const redirect_flag = ZigString.init("redirect").toJS(globalThis);
try assertStreamingDisabled(globalThis, async_local_storage, "Response.redirect");
try JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis);
}
return response_js;
}
pub fn constructRender(
globalThis: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!JSValue {
const arguments = callframe.arguments_old(2);
const vm = globalThis.bunVM();
// Check if dev_server_async_local_storage is set
const async_local_storage = vm.dev_server_async_local_storage.get() orelse {
return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{});
};
try assertStreamingDisabled(globalThis, async_local_storage, "Response.render");
// Validate arguments
if (arguments.len < 1) {
return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{});
}
const path_arg = arguments.ptr[0];
if (!path_arg.isString()) {
return globalThis.throwInvalidArguments("Response.render() path must be a string", .{});
}
// Get the path string
const path_str = try path_arg.toSlice(globalThis, bun.default_allocator);
// Duplicate the path string so it persists
const path_copy = bun.default_allocator.dupe(u8, path_str.slice()) catch {
path_str.deinit();
return globalThis.throwOutOfMemory();
};
path_str.deinit();
// Create a Response with Render body
var response = bun.new(Response, Response{
.body = Body{
.value = .{
.Render = .{
.path = path_copy,
},
},
},
.init = Response.Init{
.status_code = 200,
},
});
const response_js = response.toJS(globalThis);
response_js.ensureStillAlive();
// Store the render path and params on the response for later use
// When React tries to render this component, we'll check for these and throw RenderAbortError
response_js.put(globalThis, "__renderPath", path_arg);
const params_arg = if (arguments.len >= 2) arguments.ptr[1] else JSValue.jsNull();
response_js.put(globalThis, "__renderParams", params_arg);
// TODO: this is terrible
// Create a simple wrapper function that will be called by React
// This needs to be handled specially in transformToReactElementWithOptions
// We'll pass a special marker as the component to indicate this is a render redirect
const render_marker = ZigString.init("__bun_render_redirect__").toJS(globalThis);
// Transform the Response to act as a React element
// The C++ code will need to check for this special marker
try JSValue.transformToReactElementWithOptions(response_js, render_marker, params_arg, globalThis);
return response_js;
}
pub fn constructError(
globalThis: *jsc.JSGlobalObject,
_: *jsc.CallFrame,
) bun.JSError!JSValue {
const response = bun.new(
Response,
Response{
.init = Response.Init{
.status_code = 0,
},
.body = Body{
.value = .{ .Empty = {} },
},
},
);
return response.toJS(globalThis);
}
pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response {
return constructorImpl(globalThis, callframe, this_value, false);
}
pub fn ResponseClass__constructForSSR(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, thisValue: jsc.JSValue) callconv(jsc.conv) ?*anyopaque {
return @as(*Response, Response.constructor(globalObject, callFrame, thisValue) catch |err| switch (err) {
error.JSError => return null,
error.OutOfMemory => {
globalObject.throwOutOfMemory() catch {};
return null;
},
});
}
comptime {
@export(&ResponseClass__constructForSSR, .{ .name = "ResponseClass__constructForSSR" });
}
pub fn constructorForSSR(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response {
return constructorImpl(globalThis, callframe, this_value);
}
pub fn constructorImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue, bake_ssr_response_enabled: bool) bun.JSError!*Response {
var arguments = callframe.argumentsAsArray(2);
if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) {
if (arguments[0].as(Blob)) |blob| {
if (blob.isS3()) {
if (!arguments[1].isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("new Response(s3File) do not support ResponseInit options", .{});
}
var response: Response = .{
.init = Response.Init{
.status_code = 302,
},
.body = Body{
.value = .{ .Empty = {} },
},
.url = bun.String.empty,
};
const credentials = blob.store.?.data.s3.getCredentials();
const result = credentials.signRequest(.{
.path = blob.store.?.data.s3.path(),
.method = .GET,
}, false, .{ .expires = 15 * 60 }) catch |sign_err| {
return s3.throwSignError(sign_err, globalThis);
};
defer result.deinit();
response.init.headers = try response.getOrCreateHeaders(globalThis);
response.redirected = true;
var headers_ref = response.init.headers.?;
try headers_ref.put(.Location, result.url, globalThis);
return bun.new(Response, response);
}
}
// Special case for bake: allow `return new Response(<jsx> ... </jsx>, { ... }`
// inside of a react component
if (bake_ssr_response_enabled and globalThis.allowJSXInResponseConstructor()) {
_ = this_value;
// const arg = arguments[0];
// // Check if it's a JSX element (object with $$typeof)
// if (try arg.isJSXElement(globalThis)) {
// const vm = globalThis.bunVM();
// if (vm.dev_server_async_local_storage.get()) |async_local_storage| {
// try assertStreamingDisabled(globalThis, async_local_storage, "new Response(<jsx />, { ... })");
// }
// // Pass the response options (arguments[1]) to transformToReactElement
// // so it can store them for later use when the component is rendered
// const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined;
// try JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis);
// }
}
}
var init: Init = (brk: {
if (arguments[1].isUndefinedOrNull()) {
break :brk Init{
.status_code = 200,
.headers = null,
};
}
if (arguments[1].isObject()) {
break :brk try Init.init(globalThis, arguments[1]) orelse unreachable;
}
if (!globalThis.hasException()) {
return globalThis.throwInvalidArguments("Failed to construct 'Response': The provided body value is not of type 'ResponseInit'", .{});
}
return error.JSError;
});
errdefer init.deinit(bun.default_allocator);
if (globalThis.hasException()) {
return error.JSError;
}
var body: Body = brk: {
if (arguments[0].isUndefinedOrNull()) {
break :brk Body{
.value = Body.Value{ .Null = {} },
};
}
break :brk try Body.extract(globalThis, arguments[0]);
};
errdefer body.deinit(bun.default_allocator);
if (globalThis.hasException()) {
return error.JSError;
}
var response = bun.new(Response, Response{
.body = body,
.init = init,
});
if (response.body.value == .Blob and
response.init.headers != null and
response.body.value.Blob.content_type.len > 0 and
!response.init.headers.?.fastHas(.ContentType))
{
try response.init.headers.?.put(.ContentType, response.body.value.Blob.content_type, globalThis);
}
response.calculateEstimatedByteSize();
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) bun.JSError!Init {
var that = this;
const headers = this.headers;
if (headers) |head| {
that.headers = try head.cloneThis(ctx);
}
that.status_text = this.status_text.clone();
return that;
}
pub fn init(globalThis: *JSGlobalObject, response_init: jsc.JSValue) bun.JSError!?Init {
var result = Init{ .status_code = 200 };
errdefer {
result.deinit(bun.default_allocator);
}
if (!response_init.isCell())
return null;
const js_type = response_init.jsType();
if (!js_type.isObject()) {
return null;
}
if (js_type == .DOMWrapper) {
// fast path: it's a Request object or a Response object
// we can skip calling JS getters
if (response_init.asDirect(Request)) |req| {
if (req.getFetchHeadersUnlessEmpty()) |headers| {
result.headers = try headers.cloneThis(globalThis);
}
result.method = req.method;
return result;
}
if (response_init.asDirect(Response)) |resp| {
return try resp.init.clone(globalThis);
}
}
if (globalThis.hasException()) {
return error.JSError;
}
if (try response_init.fastGet(globalThis, .headers)) |headers| {
if (headers.as(FetchHeaders)) |orig| {
if (!orig.isEmpty()) {
result.headers = try orig.cloneThis(globalThis);
}
} else {
result.headers = try FetchHeaders.createFromJS(globalThis, headers);
}
}
if (globalThis.hasException()) {
return error.JSError;
}
if (try response_init.fastGet(globalThis, .status)) |status_value| {
const number = try status_value.coerceToInt64(globalThis);
if ((200 <= number and number < 600) or number == 101) {
result.status_code = @as(u16, @truncate(@as(u32, @intCast(number))));
} else {
if (!globalThis.hasException()) {
const err = globalThis.createRangeErrorInstance("The status provided ({d}) must be 101 or in the range of [200, 599]", .{number});
return globalThis.throwValue(err);
}
return error.JSError;
}
}
if (globalThis.hasException()) {
return error.JSError;
}
if (try response_init.getTruthy(globalThis, "statusText")) |status_text| {
result.status_text = try bun.String.fromJS(status_text, globalThis);
}
if (try response_init.getTruthy(globalThis, "method")) |method_value| {
if (try Method.fromJS(globalThis, method_value)) |method| {
result.method = method;
}
}
return result;
}
pub fn deinit(this: *Init, _: std.mem.Allocator) void {
if (this.headers) |headers| {
this.headers = null;
headers.deref();
}
this.status_text.deref();
this.status_text = bun.String.empty;
}
};
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(_: *jsc.JSGlobalObject, status: u16) Response {
return bun.new(Response, .{
.body = Body{
.value = Body.Value{ .Null = {} },
},
.init = Init{
.status_code = status,
},
});
}
fn assertStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, display_function: []const u8) bun.JSError!void {
if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{});
const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{});
const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{});
const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{});
if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" fied must be a boolean", .{});
if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{display_function});
}
/// https://developer.mozilla.org/en-US/docs/Web/API/Headers
// TODO: move to http.zig. this has nothing to do with jsc or WebCore
const string = []const u8;
const std = @import("std");
const Method = @import("../../http/Method.zig").Method;
const bun = @import("bun");
const Output = bun.Output;
const default_allocator = bun.default_allocator;
const s3 = bun.S3;
const FetchHeaders = bun.webcore.FetchHeaders;
const http = bun.http;
const MimeType = bun.http.MimeType;
const jsc = bun.jsc;
const JSGlobalObject = jsc.JSGlobalObject;
const JSValue = jsc.JSValue;
const ZigString = jsc.ZigString;
const Request = jsc.WebCore.Request;
const Blob = jsc.WebCore.Blob;
const InternalBlob = jsc.WebCore.Blob.Internal;
const Body = jsc.WebCore.Body;
const BodyMixin = jsc.WebCore.Body.Mixin;