mirror of
https://github.com/oven-sh/bun
synced 2026-02-14 12:51:54 +00:00
921 lines
32 KiB
Zig
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;
|