aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-07-30 23:51:43 -0700
committerGravatar GitHub <noreply@github.com> 2023-07-30 23:51:43 -0700
commit9ecae59bbb1a302977afa94fd879a0c6f8d6195f (patch)
tree7bfff6d9c3264b8d1fce85f63891a1f3e5833a52
parent2ea7290172da21f4a9cd58232bf9c1ea0997d6e2 (diff)
downloadbun-9ecae59bbb1a302977afa94fd879a0c6f8d6195f.tar.gz
bun-9ecae59bbb1a302977afa94fd879a0c6f8d6195f.tar.zst
bun-9ecae59bbb1a302977afa94fd879a0c6f8d6195f.zip
Fix memory leak in response.clone(), further reduce memory usage of Request & Response (#3902)
* Atomize respsone.url & response.statusText * Fix warning * Atomize Request & Response URLs when possible * Fix memory leak in response.clone() bun/bench/snippets on  jarred/atomize ❯ mem bun --smol request-response-clone.mjs cpu: Apple M1 Max runtime: bun 0.7.2 (arm64-darwin) benchmark time (avg) (min … max) p75 p99 p995 -------------------------------------------------------- ----------------------------- req.clone().url 77.3 ns/iter (40.35 ns … 222.64 ns) 91.53 ns 128.11 ns 172.78 ns resp.clone().url 162.43 ns/iter (116 ns … 337.77 ns) 177.4 ns 232.38 ns 262.65 ns Peak memory usage: 60 MB bun/bench/snippets on  jarred/atomize ❯ mem bun-0.7.1 --smol request-response-clone.mjs cpu: Apple M1 Max runtime: bun 0.7.1 (arm64-darwin) benchmark time (avg) (min … max) p75 p99 p995 -------------------------------------------------------- ----------------------------- req.clone().url 115.85 ns/iter (80.35 ns … 247.39 ns) 128.19 ns 181.93 ns 207.23 ns resp.clone().url 252.32 ns/iter (202.6 ns … 351.07 ns) 266.56 ns 325.88 ns 334.73 ns Peak memory usage: 1179 MB * Update tests * Update js_ast.zig * Update test --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--bench/snippets/request-response-clone.mjs15
-rw-r--r--bench/snippets/resposne-constructor.mjs1
-rw-r--r--src/bun.js/api/filesystem_router.zig9
-rw-r--r--src/bun.js/api/html_rewriter.zig4
-rw-r--r--src/bun.js/api/server.zig47
-rw-r--r--src/bun.js/bindings/BunString.cpp25
-rw-r--r--src/bun.js/bindings/bindings.cpp2
-rw-r--r--src/bun.js/webcore/encoding.zig3
-rw-r--r--src/bun.js/webcore/request.zig126
-rw-r--r--src/bun.js/webcore/response.zig50
-rw-r--r--src/js_ast.zig1
-rw-r--r--src/string.zig52
-rw-r--r--test/js/bun/http/bun-server.test.ts4
-rw-r--r--test/js/bun/util/inspect.test.js2
-rw-r--r--test/js/web/fetch/body-stream.test.ts5
-rw-r--r--test/js/web/fetch/fetch.test.ts40
16 files changed, 256 insertions, 130 deletions
diff --git a/bench/snippets/request-response-clone.mjs b/bench/snippets/request-response-clone.mjs
new file mode 100644
index 000000000..05a980656
--- /dev/null
+++ b/bench/snippets/request-response-clone.mjs
@@ -0,0 +1,15 @@
+// This mostly exists to check for a memory leak in response.clone()
+import { bench, run } from "./runner.mjs";
+
+const req = new Request("http://localhost:3000/");
+const resp = await fetch("http://example.com");
+
+bench("req.clone().url", () => {
+ return req.clone().url;
+});
+
+bench("resp.clone().url", () => {
+ return resp.clone().url;
+});
+
+await run();
diff --git a/bench/snippets/resposne-constructor.mjs b/bench/snippets/resposne-constructor.mjs
new file mode 100644
index 000000000..a15892804
--- /dev/null
+++ b/bench/snippets/resposne-constructor.mjs
@@ -0,0 +1 @@
+for (let i = 0; i < 9999999; i++) new Request("http://aaaaaaaaaaaaaaaaaaaaa");
diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig
index 216f66b7f..edb666040 100644
--- a/src/bun.js/api/filesystem_router.zig
+++ b/src/bun.js/api/filesystem_router.zig
@@ -75,6 +75,8 @@ const DeprecatedGlobalRouter = struct {
};
var path_: ?ZigString.Slice = null;
+ var req_url_slice: ZigString.Slice = .{};
+ defer req_url_slice.deinit();
var pathname: string = "";
defer {
if (path_) |path| {
@@ -88,7 +90,8 @@ const DeprecatedGlobalRouter = struct {
var url = URL.parse(path_.?.slice());
pathname = url.pathname;
} else if (arg.as(Request)) |req| {
- var url = URL.parse(req.url);
+ req_url_slice = req.url.toUTF8(bun.default_allocator);
+ var url = URL.parse(req_url_slice.slice());
pathname = url.pathname;
}
@@ -389,11 +392,11 @@ pub const FileSystemRouter = struct {
if (argument.isCell()) {
if (argument.as(JSC.WebCore.Request)) |req| {
req.ensureURL() catch unreachable;
- break :brk ZigString.Slice.fromUTF8NeverFree(req.url).clone(globalThis.allocator()) catch unreachable;
+ break :brk req.url.toUTF8(globalThis.allocator());
}
if (argument.as(JSC.WebCore.Response)) |resp| {
- break :brk ZigString.Slice.fromUTF8NeverFree(resp.url).clone(globalThis.allocator()) catch unreachable;
+ break :brk resp.url.toUTF8(globalThis.allocator());
}
}
diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig
index 92e9790c6..f754e3e23 100644
--- a/src/bun.js/api/html_rewriter.zig
+++ b/src/bun.js/api/html_rewriter.zig
@@ -454,8 +454,8 @@ pub const HTMLRewriter = struct {
result.body.init.headers = headers.cloneThis(global);
}
- result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable;
- result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable;
+ result.url = original.url.clone();
+ result.status_text = original.status_text.clone();
var input = original.body.value.useAsAnyBlob();
sink.input = input;
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig
index 801745aee..10afa4ac1 100644
--- a/src/bun.js/api/server.zig
+++ b/src/bun.js/api/server.zig
@@ -1069,7 +1069,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
byte_stream: ?*JSC.WebCore.ByteStream = null,
/// Used in errors
- pathname: []const u8 = "",
+ pathname: bun.String = bun.String.empty,
/// Used either for temporary blob data or fallback
/// When the response body is a temporary value
@@ -1550,10 +1550,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
stream.unpipe();
}
- if (this.pathname.len > 0) {
- ctxLog("finalizeWithoutDeinit: this.pathname.len > 0 null", .{});
- this.allocator.free(bun.constStrToU8(this.pathname));
- this.pathname = "";
+ if (!this.pathname.isEmpty()) {
+ this.pathname.deref();
+ this.pathname = bun.String.empty;
}
// if we are waiting for the body yet and the request was not aborted we can safely clear the onData callback
@@ -2235,7 +2234,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
request_object.uws_request = req;
request_object.ensureURL() catch {
- request_object.url = "";
+ request_object.url = bun.String.empty;
};
// we have to clone the request headers here since they will soon belong to a different request
@@ -2244,7 +2243,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
}
if (comptime debug_mode) {
- ctx.pathname = bun.default_allocator.dupe(u8, request_object.url) catch unreachable;
+ ctx.pathname = request_object.url.clone();
}
// This object dies after the stack frame is popped
@@ -2596,15 +2595,28 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
runErrorHandlerWithStatusCode(this, value, 500);
}
- fn ensurePathname(this: *RequestContext) []const u8 {
- if (this.pathname.len > 0)
- return this.pathname;
+ const PathnameFormatter = struct {
+ ctx: *RequestContext,
+
+ pub fn format(formatter: @This(), comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
+ var this = formatter.ctx;
+
+ if (!this.pathname.isEmpty()) {
+ try this.pathname.format(fmt, opts, writer);
+ return;
+ }
- if (!this.flags.has_abort_handler) {
- return this.req.url();
+ if (!this.flags.has_abort_handler) {
+ try writer.writeAll(this.req.url());
+ return;
+ }
+
+ try writer.writeAll("/");
}
+ };
- return "/";
+ fn ensurePathname(this: *RequestContext) PathnameFormatter {
+ return .{ .ctx = this };
}
pub inline fn shouldCloseConnection(this: *const RequestContext) bool {
@@ -2625,7 +2637,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
vm.log,
error.ExceptionOcurred,
exception_list.toOwnedSlice() catch @panic("TODO"),
- "<r><red>{s}<r> - <b>{s}<r> failed",
+ "<r><red>{s}<r> - <b>{}<r> failed",
.{ @as(string, @tagName(this.method)), this.ensurePathname() },
);
} else {
@@ -4900,8 +4912,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type {
}
existing_request = Request{
- .url = url.href,
- .url_was_allocated = true,
+ .url = bun.String.create(url.href),
.headers = headers,
.body = JSC.WebCore.InitRequestBodyValue(body) catch unreachable,
.method = method,
@@ -4943,7 +4954,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type {
}
if (response_value.as(JSC.WebCore.Response)) |resp| {
- resp.url = this.allocator.dupe(u8, existing_request.url) catch unreachable;
+ resp.url = existing_request.url.clone();
}
return JSC.JSPromise.resolvedPromiseValue(ctx, response_value).asObjectRef();
}
@@ -5295,7 +5306,6 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type {
}
request_object.* = .{
- .url = "",
.method = ctx.method,
.uws_request = req,
.https = ssl_enabled,
@@ -5398,7 +5408,6 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type {
}
request_object.* = .{
- .url = "",
.method = ctx.method,
.uws_request = req,
.upgrader = ctx,
diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp
index 5eb5f9f9b..7864ee94e 100644
--- a/src/bun.js/bindings/BunString.cpp
+++ b/src/bun.js/bindings/BunString.cpp
@@ -6,6 +6,7 @@
#include "wtf/text/ExternalStringImpl.h"
#include "GCDefferalContext.h"
#include <JavaScriptCore/JSONObject.h>
+#include <wtf/text/AtomString.h>
using namespace JSC;
@@ -25,11 +26,23 @@ extern "C" void Bun__WTFStringImpl__ref(WTF::StringImpl* impl)
extern "C" bool BunString__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, BunString* bunString)
{
+
JSC::JSValue value = JSC::JSValue::decode(encodedValue);
*bunString = Bun::toString(globalObject, value);
return bunString->tag != BunStringTag::Dead;
}
+extern "C" BunString BunString__createAtom(const char* bytes, size_t length)
+{
+ if (simdutf::validate_ascii(bytes, length)) {
+ auto atom = makeAtomString(String(StringImpl::createWithoutCopying(bytes, length)));
+ atom.impl()->ref();
+ return { BunStringTag::WTFStringImpl, { .wtf = atom.impl() } };
+ }
+
+ return { BunStringTag::Dead, {} };
+}
+
namespace Bun {
JSC::JSValue toJS(JSC::JSGlobalObject* globalObject, BunString bunString)
{
@@ -179,7 +192,6 @@ extern "C" BunString BunString__fromUTF16Unitialized(size_t length)
if (UNLIKELY(!ptr))
return { BunStringTag::Dead };
- impl->ref();
return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } };
}
@@ -190,7 +202,6 @@ extern "C" BunString BunString__fromLatin1Unitialized(size_t length)
auto impl = WTF::StringImpl::createUninitialized(latin1Length, ptr);
if (UNLIKELY(!ptr))
return { BunStringTag::Dead };
- impl->ref();
return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } };
}
@@ -302,10 +313,10 @@ extern "C" EncodedJSValue BunString__createArray(
extern "C" void BunString__toWTFString(BunString* bunString)
{
if (bunString->tag == BunStringTag::ZigString) {
- if (Zig::isTaggedUTF8Ptr(bunString->impl.zig.ptr)) {
- bunString->impl.wtf = Zig::toStringCopy(bunString->impl.zig).impl();
- } else {
+ if (Zig::isTaggedExternalPtr(bunString->impl.zig.ptr)) {
bunString->impl.wtf = Zig::toString(bunString->impl.zig).impl();
+ } else {
+ bunString->impl.wtf = Zig::toStringCopy(bunString->impl.zig).impl();
}
bunString->tag = BunStringTag::WTFStringImpl;
@@ -356,7 +367,7 @@ extern "C" BunString URL__getHrefFromJS(EncodedJSValue encodedValue, JSC::JSGlob
extern "C" BunString URL__getHref(BunString* input)
{
- auto str = Bun::toWTFString(*input);
+ auto&& str = Bun::toWTFString(*input);
auto url = WTF::URL(str);
if (!url.isValid() || url.isEmpty())
return { BunStringTag::Dead };
@@ -366,7 +377,7 @@ extern "C" BunString URL__getHref(BunString* input)
extern "C" WTF::URL* URL__fromString(BunString* input)
{
- auto str = Bun::toWTFString(*input);
+ auto&& str = Bun::toWTFString(*input);
auto url = WTF::URL(str);
if (!url.isValid())
return nullptr;
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index 981a475a3..b0a291c2e 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -1404,7 +1404,7 @@ double JSC__JSValue__getLengthIfPropertyExistsInternal(JSC__JSValue value, JSC__
return 0;
}
- case JSC::JSType(JSDOMWrapperType): {
+ case WebCore::JSDOMWrapperType: {
if (auto* headers = jsDynamicCast<WebCore::JSFetchHeaders*>(cell))
return static_cast<double>(jsCast<WebCore::JSFetchHeaders*>(cell)->wrapped().size());
diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig
index 7a0188aab..4fa0f4ca2 100644
--- a/src/bun.js/webcore/encoding.zig
+++ b/src/bun.js/webcore/encoding.zig
@@ -866,13 +866,11 @@ pub const Encoder = struct {
}
var str = bun.String.createUninitialized(.latin1, len) orelse return ZigString.init("Out of memory").toErrorInstance(global);
- defer str.deref();
strings.copyLatin1IntoASCII(@constCast(str.latin1()), input);
return str.toJS(global);
},
.latin1 => {
var str = bun.String.createUninitialized(.latin1, len) orelse return ZigString.init("Out of memory").toErrorInstance(global);
- defer str.deref();
@memcpy(@constCast(str.latin1()), input_ptr[0..len]);
@@ -903,7 +901,6 @@ pub const Encoder = struct {
.hex => {
var str = bun.String.createUninitialized(.latin1, len * 2) orelse return ZigString.init("Out of memory").toErrorInstance(global);
- defer str.deref();
var output = @constCast(str.latin1());
const wrote = strings.encodeBytesToHex(output, input);
std.debug.assert(wrote == output.len);
diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig
index 75d4e63cd..af212e4cf 100644
--- a/src/bun.js/webcore/request.zig
+++ b/src/bun.js/webcore/request.zig
@@ -63,8 +63,7 @@ pub fn InitRequestBodyValue(value: Body.Value) !*BodyValueRef {
}
// https://developer.mozilla.org/en-US/docs/Web/API/Request
pub const Request = struct {
- url: []const u8 = "",
- url_was_allocated: bool = false,
+ url: bun.String = bun.String.empty,
headers: ?*FetchHeaders = null,
signal: ?*AbortSignal = null,
@@ -143,7 +142,7 @@ pub const Request = struct {
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>url<d>:<r> ", enable_ansi_colors));
try this.ensureURL();
- try writer.print(comptime Output.prettyFmt("\"<b>{s}<r>\"", enable_ansi_colors), .{this.url});
+ try writer.print(comptime Output.prettyFmt("\"<b>{}<r>\"", enable_ansi_colors), .{this.url});
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
@@ -179,11 +178,10 @@ pub const Request = struct {
pub fn fromRequestContext(ctx: *RequestContext) !Request {
var req = Request{
- .url = bun.asByteSlice(ctx.getFullURL()),
+ .url = bun.String.create(ctx.full_url),
.body = try InitRequestBodyValue(.{ .Null = {} }),
.method = ctx.method,
.headers = FetchHeaders.createFromPicoHeaders(ctx.request.headers),
- .url_was_allocated = true,
};
return req;
}
@@ -271,9 +269,8 @@ pub const Request = struct {
this.headers = null;
}
- if (this.url_was_allocated) {
- bun.default_allocator.free(bun.constStrToU8(this.url));
- }
+ this.url.deref();
+ this.url = bun.String.empty;
if (this.signal) |signal| {
_ = signal.unref();
@@ -320,12 +317,12 @@ pub const Request = struct {
return .zero;
};
- return ZigString.init(this.url).withEncoding().toValueGC(globalObject);
+ return this.url.toJS(globalObject);
}
pub fn sizeOfURL(this: *const Request) usize {
- if (this.url.len > 0)
- return this.url.len;
+ if (this.url.length() > 0)
+ return this.url.length();
if (this.uws_request) |req| {
const req_url = req.url();
@@ -352,7 +349,7 @@ pub const Request = struct {
}
pub fn ensureURL(this: *Request) !void {
- if (this.url.len > 0) return;
+ if (!this.url.isEmpty()) return;
if (this.uws_request) |req| {
const req_url = req.url();
@@ -362,16 +359,53 @@ pub const Request = struct {
.is_https = this.https,
.host = host,
};
- const url = try std.fmt.allocPrint(bun.default_allocator, "{s}{any}{s}", .{
+ const url_bytelength = std.fmt.count("{s}{any}{s}", .{
this.getProtocol(),
fmt,
req_url,
});
+
if (comptime Environment.allow_assert) {
- std.debug.assert(this.sizeOfURL() == url.len);
+ std.debug.assert(this.sizeOfURL() == url_bytelength);
+ }
+
+ if (url_bytelength < 64) {
+ var buffer: [64]u8 = undefined;
+ const url = std.fmt.bufPrint(&buffer, "{s}{any}{s}", .{
+ this.getProtocol(),
+ fmt,
+ req_url,
+ }) catch @panic("Unexpected error while printing URL");
+
+ if (comptime Environment.allow_assert) {
+ std.debug.assert(this.sizeOfURL() == url.len);
+ }
+
+ if (bun.String.tryCreateAtom(url)) |atomized| {
+ this.url = atomized;
+ return;
+ }
+ }
+
+ if (strings.isAllASCII(host) and strings.isAllASCII(req_url)) {
+ this.url = bun.String.createUninitializedLatin1(url_bytelength);
+ var bytes = @constCast(this.url.byteSlice());
+ const url = std.fmt.bufPrint(bytes, "{s}{any}{s}", .{
+ this.getProtocol(),
+ fmt,
+ req_url,
+ }) catch @panic("Unexpected error while printing URL");
+ _ = url;
+ } else {
+ // slow path
+ var temp_url = std.fmt.allocPrint(bun.default_allocator, "{s}{any}{s}", .{
+ this.getProtocol(),
+ fmt,
+ req_url,
+ }) catch unreachable;
+ defer bun.default_allocator.free(temp_url);
+ this.url = bun.String.create(temp_url);
}
- this.url = url;
- this.url_was_allocated = true;
return;
}
}
@@ -379,8 +413,7 @@ pub const Request = struct {
if (comptime Environment.allow_assert) {
std.debug.assert(this.sizeOfURL() == req_url.len);
}
- this.url = try bun.default_allocator.dupe(u8, req_url);
- this.url_was_allocated = true;
+ this.url = bun.String.create(req_url);
}
}
@@ -432,18 +465,13 @@ pub const Request = struct {
url_or_object.as(JSC.DOMURL) != null;
if (is_first_argument_a_url) {
- const slice = arguments[0].toSliceOrNull(globalThis) orelse {
+ const str = bun.String.tryFromJS(arguments[0], globalThis) orelse {
req.finalizeWithoutDeinit();
_ = req.body.unref();
return null;
};
- req.url = (slice.cloneIfNeeded(globalThis.allocator()) catch {
- req.finalizeWithoutDeinit();
- _ = req.body.unref();
- return null;
- }).slice();
- req.url_was_allocated = req.url.len > 0;
- if (req.url.len > 0)
+ req.url = str;
+ if (!req.url.isEmpty())
fields.insert(.url);
} else if (!url_or_object_type.isObject()) {
globalThis.throw("Failed to construct 'Request': expected non-empty string or object", .{});
@@ -510,9 +538,8 @@ pub const Request = struct {
}
if (!fields.contains(.url)) {
- if (response.url.len > 0) {
- req.url = globalThis.allocator().dupe(u8, response.url) catch unreachable;
- req.url_was_allocated = true;
+ if (!response.url.isEmpty()) {
+ req.url = response.url;
fields.insert(.url);
}
}
@@ -544,29 +571,21 @@ pub const Request = struct {
if (!fields.contains(.url)) {
if (value.fastGet(globalThis, .url)) |url| {
- req.url = (url.toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
- return null;
- }).slice();
- req.url_was_allocated = req.url.len > 0;
- if (req.url.len > 0)
+ req.url = bun.String.fromJS(url, globalThis);
+ if (!req.url.isEmpty())
fields.insert(.url);
// first value
} else if (@intFromEnum(value) == @intFromEnum(values_to_try[values_to_try.len - 1]) and !is_first_argument_a_url and
value.implementsToString(globalThis))
{
- const slice = value.toSliceOrNull(globalThis) orelse {
+ const str = bun.String.tryFromJS(value, globalThis) orelse {
req.finalizeWithoutDeinit();
_ = req.body.unref();
return null;
};
- req.url = (slice.cloneIfNeeded(globalThis.allocator()) catch {
- req.finalizeWithoutDeinit();
- _ = req.body.unref();
- return null;
- }).slice();
- req.url_was_allocated = req.url.len > 0;
- if (req.url.len > 0)
+ req.url = str;
+ if (!req.url.isEmpty())
fields.insert(.url);
}
}
@@ -607,20 +626,24 @@ pub const Request = struct {
}
}
- if (req.url.len == 0) {
+ if (req.url.isEmpty()) {
globalThis.throw("Failed to construct 'Request': url is required.", .{});
req.finalizeWithoutDeinit();
_ = req.body.unref();
return null;
}
- const parsed_url = ZigURL.parse(req.url);
- if (parsed_url.hostname.len == 0) {
- globalThis.throw("Failed to construct 'Request': Invalid URL (missing a hostname)", .{});
+ // Note that the string is going to be ref'd here, so we don't need to ref it above.
+ const href = JSC.URL.hrefFromString(req.url);
+ if (href.isEmpty()) {
+ globalThis.throw("Failed to construct 'Request': Invalid URL \"{}\"", .{
+ req.url,
+ });
req.finalizeWithoutDeinit();
_ = req.body.unref();
return null;
}
+ req.url = href;
if (req.body.value == .Blob and
req.headers != null and
@@ -717,23 +740,18 @@ pub const Request = struct {
globalThis: *JSGlobalObject,
preserve_url: bool,
) void {
+ _ = allocator;
this.ensureURL() catch {};
var body = InitRequestBodyValue(this.body.value.clone(globalThis)) catch {
globalThis.throw("Failed to clone request", .{});
return;
};
-
- const original_url = req.url;
+ var original_url = req.url;
req.* = Request{
.body = body,
- .url = if (preserve_url) original_url else allocator.dupe(u8, this.url) catch {
- _ = body.unref();
- globalThis.throw("Failed to clone request", .{});
- return;
- },
- .url_was_allocated = if (preserve_url) req.url_was_allocated else true,
+ .url = if (preserve_url) original_url else this.url.dupeRef(),
.method = this.method,
.headers = this.cloneHeaders(globalThis),
};
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
index e9ba24fc5..3341c6eaf 100644
--- a/src/bun.js/webcore/response.zig
+++ b/src/bun.js/webcore/response.zig
@@ -59,8 +59,8 @@ pub const Response = struct {
allocator: std.mem.Allocator,
body: Body,
- url: string = "",
- status_text: string = "",
+ url: bun.String = bun.String.empty,
+ status_text: bun.String = bun.String.empty,
redirected: bool = false,
// We must report a consistent value for this
@@ -85,7 +85,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.len + this.status_text.len + @sizeOf(Response)),
+ @intCast(this.body.value.estimatedSize() + this.url.byteSlice().len + this.status_text.byteSlice().len + @sizeOf(Response)),
);
break :brk this.reported_estimated_size.?;
};
@@ -135,7 +135,7 @@ pub const Response = struct {
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>{s}<r>", enable_ansi_colors), .{this.url});
+ try writer.print(comptime Output.prettyFmt("<r><b>{}<r>", enable_ansi_colors), .{this.url});
try writer.writeAll("\"");
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
@@ -148,7 +148,7 @@ pub const Response = struct {
try formatter.writeIndent(Writer, writer);
try writer.writeAll(comptime Output.prettyFmt("<r>statusText<d>:<r> ", enable_ansi_colors));
- try JSPrinter.writeJSONString(this.status_text, Writer, writer, .ascii);
+ try writer.print(comptime Output.prettyFmt("<r>\"<b>{}<r>\"", enable_ansi_colors), .{this.status_text});
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
@@ -175,7 +175,7 @@ pub const Response = struct {
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/url
- return ZigString.init(this.url).toValueGC(globalThis);
+ return this.url.toJS(globalThis);
}
pub fn getResponseType(
@@ -194,7 +194,7 @@ pub const Response = struct {
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
// https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
- return ZigString.init(this.status_text).withEncoding().toValueGC(globalThis);
+ return this.status_text.toJS(globalThis);
}
pub fn getRedirected(
@@ -241,12 +241,7 @@ pub const Response = struct {
_: *JSC.CallFrame,
) callconv(.C) JSValue {
var cloned = this.clone(getAllocator(globalThis), globalThis);
- const val = Response.makeMaybePooled(globalThis, cloned);
- if (this.body.init.headers) |headers| {
- cloned.body.init.headers = headers.cloneThis(globalThis);
- }
-
- return val;
+ return Response.makeMaybePooled(globalThis, cloned);
}
pub fn makeMaybePooled(globalObject: *JSC.JSGlobalObject, ptr: *Response) JSValue {
@@ -262,8 +257,8 @@ pub const Response = struct {
new_response.* = Response{
.allocator = allocator,
.body = this.body.clone(globalThis),
- .url = allocator.dupe(u8, this.url) catch unreachable,
- .status_text = allocator.dupe(u8, this.status_text) catch unreachable,
+ .url = this.url.clone(),
+ .status_text = this.status_text.clone(),
.redirected = this.redirected,
};
}
@@ -289,13 +284,8 @@ pub const Response = struct {
var allocator = this.allocator;
- if (this.status_text.len > 0) {
- allocator.free(this.status_text);
- }
-
- if (this.url.len > 0) {
- allocator.free(this.url);
- }
+ this.status_text.deref();
+ this.url.deref();
allocator.destroy(this);
}
@@ -381,7 +371,7 @@ pub const Response = struct {
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
- .url = "",
+ .url = bun.String.empty,
};
const json_value = args.nextEat() orelse JSC.JSValue.zero;
@@ -443,7 +433,7 @@ pub const Response = struct {
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
- .url = "",
+ .url = bun.String.empty,
};
const url_string_value = args.nextEat() orelse JSC.JSValue.zero;
@@ -487,7 +477,6 @@ pub const Response = struct {
.value = .{ .Empty = {} },
},
.allocator = getAllocator(globalThis),
- .url = "",
};
return response.toJS(globalThis);
@@ -534,7 +523,6 @@ pub const Response = struct {
response.* = Response{
.body = body,
.allocator = getAllocator(globalThis),
- .url = "",
};
if (response.body.value == .Blob and
@@ -821,8 +809,8 @@ pub const Fetch = struct {
const http_response = this.result.response;
return Response{
.allocator = allocator,
- .url = allocator.dupe(u8, this.result.href) catch unreachable,
- .status_text = allocator.dupe(u8, http_response.status) catch unreachable,
+ .url = bun.String.createAtomIfPossible(this.result.href),
+ .status_text = bun.String.createAtomIfPossible(http_response.status),
.redirected = this.result.redirected,
.body = .{
.init = .{
@@ -1041,7 +1029,7 @@ pub const Fetch = struct {
if (first_arg.as(Request)) |request| {
request.ensureURL() catch unreachable;
- if (request.url.len == 0) {
+ if (request.url.isEmpty()) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx);
// clean hostname if any
if (hostname) |host| {
@@ -1050,7 +1038,7 @@ pub const Fetch = struct {
return JSPromise.rejectedPromiseValue(globalThis, err);
}
- url = ZigURL.fromUTF8(bun.default_allocator, request.url) catch {
+ url = ZigURL.fromString(bun.default_allocator, request.url) catch {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx);
// clean hostname if any
if (hostname) |host| {
@@ -1377,7 +1365,7 @@ pub const Fetch = struct {
.value = .{ .Blob = bun_file },
},
.allocator = bun.default_allocator,
- .url = file_url_string.toOwnedSlice(bun.default_allocator) catch @panic("out of memory"),
+ .url = file_url_string.clone(),
};
return JSPromise.resolvedPromiseValue(globalThis, response.toJS(globalThis));
diff --git a/src/js_ast.zig b/src/js_ast.zig
index 358fe3197..d25c494fa 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -2367,7 +2367,6 @@ pub const E = struct {
if (s.is_utf16) {
var out = bun.String.createUninitializedUTF16(s.len());
- defer out.deref();
@memcpy(@constCast(out.utf16()), s.slice16());
return out.toJS(globalObject);
}
diff --git a/src/string.zig b/src/string.zig
index 1e2ee752e..4fb6e001a 100644
--- a/src/string.zig
+++ b/src/string.zig
@@ -276,6 +276,10 @@ pub const String = extern struct {
extern fn BunString__fromLatin1Unitialized(len: usize) String;
extern fn BunString__fromUTF16Unitialized(len: usize) String;
+ pub fn isGlobal(this: String) bool {
+ return this.tag == Tag.ZigString and this.value.ZigString.isGloballyAllocated();
+ }
+
pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 {
switch (this.tag) {
.ZigString => return try this.value.ZigString.toOwnedSlice(allocator),
@@ -339,6 +343,54 @@ pub const String = extern struct {
return this;
}
+ pub fn clone(this: String) String {
+ if (this.tag == .WTFStringImpl) {
+ return this.dupeRef();
+ }
+
+ if (this.isEmpty()) {
+ return this;
+ }
+
+ if (this.isUTF16()) {
+ var new = createUninitializedUTF16(this.length());
+ @memcpy(@constCast(new.byteSlice()), this.byteSlice());
+ return new;
+ }
+
+ return create(this.byteSlice());
+ }
+
+ extern fn BunString__createAtom(bytes: [*]const u8, len: usize) String;
+
+ /// May return .Dead if the string is too long or non-ascii.
+ pub fn createAtom(bytes: []const u8) String {
+ JSC.markBinding(@src());
+ return BunString__createAtom(bytes.ptr, bytes.len);
+ }
+
+ pub fn tryCreateAtom(bytes: []const u8) ?String {
+ const atom = createAtom(bytes);
+ if (atom.isEmpty()) {
+ return null;
+ }
+
+ return atom;
+ }
+
+ /// Atomized strings are interned strings
+ /// They're de-duplicated in a threadlocal hash table
+ /// They cannot be used from other threads.
+ pub fn createAtomIfPossible(bytes: []const u8) String {
+ if (bytes.len < 64) {
+ if (tryCreateAtom(bytes)) |atom| {
+ return atom;
+ }
+ }
+
+ return create(bytes);
+ }
+
pub fn utf8ByteLength(this: String) usize {
return switch (this.tag) {
.WTFStringImpl => this.value.WTFStringImpl.utf8ByteLength(),
diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts
index 37675c25d..9fd97e3cb 100644
--- a/test/js/bun/http/bun-server.test.ts
+++ b/test/js/bun/http/bun-server.test.ts
@@ -205,7 +205,7 @@ describe("Server", () => {
},
});
try {
- const url = `http://${server.hostname}:${server.port}`;
+ const url = `http://${server.hostname}:${server.port}/`;
const response = await server.fetch(url);
expect(await response.text()).toBe("Hello World!");
expect(response.status).toBe(200);
@@ -222,7 +222,7 @@ describe("Server", () => {
},
});
try {
- const url = `http://${server.hostname}:${server.port}`;
+ const url = `http://${server.hostname}:${server.port}/`;
const response = await server.fetch(new Request(url));
expect(await response.text()).toBe("Hello World!");
expect(response.status).toBe(200);
diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js
index e91204c69..4d5165543 100644
--- a/test/js/bun/util/inspect.test.js
+++ b/test/js/bun/util/inspect.test.js
@@ -107,7 +107,7 @@ it("Request object", () => {
`
Request (0 KB) {
method: "GET",
- url: "https://example.com",
+ url: "https://example.com/",
headers: Headers {}
}`.trim(),
);
diff --git a/test/js/web/fetch/body-stream.test.ts b/test/js/web/fetch/body-stream.test.ts
index 64b3434e1..8e2baf92a 100644
--- a/test/js/web/fetch/body-stream.test.ts
+++ b/test/js/web/fetch/body-stream.test.ts
@@ -1,7 +1,6 @@
// @ts-nocheck
-import { file, gc, serve, ServeOptions } from "bun";
-import { afterAll, afterEach, describe, expect, it, test } from "bun:test";
-import { readFileSync } from "fs";
+import { gc, ServeOptions } from "bun";
+import { afterAll, describe, expect, it, test } from "bun:test";
var port = 0;
diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts
index d7fc87ade..ec73069de 100644
--- a/test/js/web/fetch/fetch.test.ts
+++ b/test/js/web/fetch/fetch.test.ts
@@ -30,6 +30,14 @@ afterAll(() => {
const payload = new Uint8Array(1024 * 1024 * 2);
crypto.getRandomValues(payload);
+it("new Request(invalid url) throws", () => {
+ expect(() => new Request("http")).toThrow();
+ expect(() => new Request("")).toThrow();
+ expect(() => new Request("http://[::1")).toThrow();
+ expect(() => new Request("https://[::1")).toThrow();
+ expect(() => new Request("!")).toThrow();
+});
+
describe("AbortSignal", () => {
beforeEach(() => {
startServer({
@@ -1165,9 +1173,9 @@ it("should not be able to parse json from empty body", () => {
});
it("#874", () => {
- expect(new Request(new Request("https://example.com"), {}).url).toBe("https://example.com");
- expect(new Request(new Request("https://example.com")).url).toBe("https://example.com");
- expect(new Request({ url: "https://example.com" }).url).toBe("https://example.com");
+ expect(new Request(new Request("https://example.com"), {}).url).toBe("https://example.com/");
+ expect(new Request(new Request("https://example.com")).url).toBe("https://example.com/");
+ expect(new Request({ url: "https://example.com" }).url).toBe("https://example.com/");
});
it("#2794", () => {
@@ -1213,3 +1221,29 @@ it("fetch() file:// works", async () => {
await Bun.file(Bun.fileURLToPath(new URL("file with space in the name.txt", import.meta.url))).text(),
);
});
+
+it("cloned response headers are independent before accessing", () => {
+ const response = new Response("hello", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+ const cloned = response.clone();
+ cloned.headers.set("content-type", "text/plain");
+ expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
+});
+
+it("cloned response headers are independent after accessing", () => {
+ const response = new Response("hello", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+
+ // create the headers
+ response.headers;
+
+ const cloned = response.clone();
+ cloned.headers.set("content-type", "text/plain");
+ expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
+});