aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--packages/bun-types/globals.d.ts2
-rw-r--r--src/bun.js/api/server.zig18
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp37
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.h1
-rw-r--r--src/bun.js/bindings/bindings.cpp23
-rw-r--r--src/bun.js/bindings/bindings.zig18
-rw-r--r--src/bun.js/bindings/generated_classes.zig26
-rw-r--r--src/bun.js/bindings/headers-cpp.h2
-rw-r--r--src/bun.js/bindings/headers.h5
-rw-r--r--src/bun.js/bindings/headers.zig3
-rw-r--r--src/bun.js/bindings/webcore/AbortSignal.cpp9
-rw-r--r--src/bun.js/bindings/webcore/AbortSignal.h1
-rw-r--r--src/bun.js/webcore/request.zig42
-rw-r--r--src/bun.js/webcore/response.classes.ts4
-rw-r--r--src/bun.js/webcore/response.zig6
-rw-r--r--src/http_client_async.zig140
-rw-r--r--test/bun.js/bun-server.test.ts24
-rw-r--r--test/bun.js/fetch.test.js397
18 files changed, 624 insertions, 134 deletions
diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts
index 69c100c05..3e677b458 100644
--- a/packages/bun-types/globals.d.ts
+++ b/packages/bun-types/globals.d.ts
@@ -1056,8 +1056,6 @@ declare class Request implements BlobInterface {
readonly referrerPolicy: ReferrerPolicy;
/**
* Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler.
- *
- * Note: this is **not implemented yet**. The cake is a lie.
*/
readonly signal: AbortSignal;
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig
index 62d8f602a..546350679 100644
--- a/src/bun.js/api/server.zig
+++ b/src/bun.js/api/server.zig
@@ -444,7 +444,6 @@ pub const ServerConfig = struct {
args.base_url = URL.parse(args.base_uri);
}
} else {
-
const hostname: string =
if (has_hostname and std.mem.span(args.hostname).len > 0) std.mem.span(args.hostname) else "0.0.0.0";
const protocol: string = if (args.ssl_config != null) "https" else "http";
@@ -1011,6 +1010,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
// User called .blob(), .json(), text(), or .arrayBuffer() on the Request object
// but we received nothing or the connection was aborted
if (request_js.as(Request)) |req| {
+ if (req.signal) |signal| {
+ // if signal is not aborted, abort the signal
+ if (!signal.aborted()) {
+ const reason = JSC.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.server.globalThis);
+ reason.ensureStillAlive();
+ _ = signal.signal(reason);
+ }
+ }
+
// the promise is pending
if (req.body == .Locked and (req.body.Locked.action != .none or req.body.Locked.promise != null)) {
this.pending_promises_for_abort += 1;
@@ -1079,6 +1087,14 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
// User called .blob(), .json(), text(), or .arrayBuffer() on the Request object
// but we received nothing or the connection was aborted
if (request_js.as(Request)) |req| {
+ if (req.signal) |signal| {
+ // if signal is not aborted, abort the signal
+ if (!signal.aborted()) {
+ const reason = JSC.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.server.globalThis);
+ reason.ensureStillAlive();
+ _ = signal.signal(reason);
+ }
+ }
// the promise is pending
if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) {
req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis);
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index 805c4c929..716f683e5 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -6201,6 +6201,9 @@ JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__referrerGetterWrap);
extern "C" JSC::EncodedJSValue RequestPrototype__getReferrerPolicy(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__referrerPolicyGetterWrap);
+extern "C" JSC::EncodedJSValue RequestPrototype__getSignal(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject);
+JSC_DECLARE_CUSTOM_GETTER(RequestPrototype__signalGetterWrap);
+
extern "C" EncodedJSValue RequestPrototype__getText(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(RequestPrototype__textCallback);
@@ -6227,6 +6230,7 @@ static const HashTableValue JSRequestPrototypeTableValues[] = {
{ "redirect"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__redirectGetterWrap, 0 } },
{ "referrer"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__referrerGetterWrap, 0 } },
{ "referrerPolicy"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__referrerPolicyGetterWrap, 0 } },
+ { "signal"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__signalGetterWrap, 0 } },
{ "text"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, RequestPrototype__textCallback, 0 } },
{ "url"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, RequestPrototype__urlGetterWrap, 0 } }
};
@@ -6507,6 +6511,37 @@ JSC_DEFINE_CUSTOM_GETTER(RequestPrototype__referrerPolicyGetterWrap, (JSGlobalOb
RELEASE_AND_RETURN(throwScope, result);
}
+JSC_DEFINE_CUSTOM_GETTER(RequestPrototype__signalGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName))
+{
+ auto& vm = lexicalGlobalObject->vm();
+ Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject);
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ JSRequest* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue));
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+ if (JSValue cachedValue = thisObject->m_signal.get())
+ return JSValue::encode(cachedValue);
+
+ JSC::JSValue result = JSC::JSValue::decode(
+ RequestPrototype__getSignal(thisObject->wrapped(), globalObject));
+ RETURN_IF_EXCEPTION(throwScope, {});
+ thisObject->m_signal.set(vm, thisObject, result);
+ RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
+}
+
+extern "C" void RequestPrototype__signalSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value)
+{
+ auto& vm = globalObject->vm();
+ auto* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue));
+ thisObject->m_signal.set(vm, thisObject, JSValue::decode(value));
+}
+
+extern "C" EncodedJSValue RequestPrototype__signalGetCachedValue(JSC::EncodedJSValue thisValue)
+{
+ auto* thisObject = jsCast<JSRequest*>(JSValue::decode(thisValue));
+ return JSValue::encode(thisObject->m_signal.get());
+}
+
JSC_DEFINE_HOST_FUNCTION(RequestPrototype__textCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = lexicalGlobalObject->vm();
@@ -6720,6 +6755,7 @@ void JSRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor)
}
visitor.append(thisObject->m_body);
visitor.append(thisObject->m_headers);
+ visitor.append(thisObject->m_signal);
visitor.append(thisObject->m_url);
}
@@ -6733,6 +6769,7 @@ void JSRequest::visitAdditionalChildren(Visitor& visitor)
visitor.append(thisObject->m_body);
visitor.append(thisObject->m_headers);
+ visitor.append(thisObject->m_signal);
visitor.append(thisObject->m_url);
;
}
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h
index 3cd3e8a1a..acf1dc140 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses.h
@@ -618,6 +618,7 @@ public:
mutable JSC::WriteBarrier<JSC::Unknown> m_body;
mutable JSC::WriteBarrier<JSC::Unknown> m_headers;
+ mutable JSC::WriteBarrier<JSC::Unknown> m_signal;
mutable JSC::WriteBarrier<JSC::Unknown> m_url;
};
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index c093fad59..8e401b4cd 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -3819,8 +3819,22 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC__JSGlobalObject* arg0
JSC::JSValue::decode(JSValue4));
}
-extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1)
-{
+extern "C" JSC__JSValue JSC__AbortSignal__create(JSC__JSGlobalObject* globalObject) {
+ Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(globalObject);
+ auto* context = thisObject->scriptExecutionContext();
+ auto abortSignal = WebCore::AbortSignal::create(context);
+
+ return JSValue::encode(toJSNewlyCreated<IDLInterface<JSC__AbortSignal>>(*globalObject, *jsCast<JSDOMGlobalObject*>(globalObject), WTFMove(abortSignal)));
+}
+extern "C" JSC__JSValue JSC__AbortSignal__toJS(JSC__AbortSignal* arg0, JSC__JSGlobalObject* globalObject) {
+ WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
+
+ return JSValue::encode(toJS<IDLInterface<JSC__AbortSignal>>(*globalObject, *jsCast<JSDOMGlobalObject*>(globalObject), *abortSignal));
+}
+
+
+extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1) {
+
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
abortSignal->signalAbort(JSC::JSValue::decode(JSValue1));
return arg0;
@@ -3851,6 +3865,10 @@ extern "C" JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0)
abortSignal->deref();
return arg0;
}
+extern "C" void JSC__AbortSignal__cleanNativeBindings(JSC__AbortSignal* arg0, void* arg1) {
+ WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
+ abortSignal->cleanNativeBindings(arg1);
+}
extern "C" JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* ctx, void (*callback)(void* ctx, JSC__JSValue reason))
{
@@ -3919,6 +3937,7 @@ extern "C" JSC__JSValue JSC__AbortSignal__createTimeoutError(const ZigString* me
return JSC::JSValue::encode(error);
}
+
#pragma mark - WebCore::DOMFormData
CPP_DECL void WebCore__DOMFormData__append(WebCore__DOMFormData* arg0, ZigString* arg1, ZigString* arg2)
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index a98d350c2..a43ff8265 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -1697,6 +1697,11 @@ pub const AbortSignal = extern opaque {
) *AbortSignal {
return cppFn("addListener", .{ this, ctx, callback });
}
+
+ pub fn cleanNativeBindings(this: *AbortSignal, ctx: ?*anyopaque) void {
+ return cppFn("cleanNativeBindings", .{ this, ctx });
+ }
+
pub fn signal(
this: *AbortSignal,
reason: JSValue,
@@ -1728,6 +1733,14 @@ pub const AbortSignal = extern opaque {
return cppFn("fromJS", .{value});
}
+ pub fn toJS(this: *AbortSignal, global: *JSGlobalObject) JSValue {
+ return cppFn("toJS", .{this, global});
+ }
+
+ pub fn create(global: *JSGlobalObject) JSValue {
+ return cppFn("create", .{ global });
+ }
+
pub fn createAbortError(message: *const ZigString, code: *const ZigString, global: *JSGlobalObject) JSValue {
return cppFn("createAbortError", .{ message, code, global });
}
@@ -1739,6 +1752,7 @@ pub const AbortSignal = extern opaque {
pub const Extern = [_][]const u8{
"createAbortError",
"createTimeoutError",
+ "create",
"ref",
"unref",
"signal",
@@ -1746,6 +1760,8 @@ pub const AbortSignal = extern opaque {
"aborted",
"addListener",
"fromJS",
+ "toJS",
+ "cleanNativeBindings"
};
};
@@ -3557,7 +3573,7 @@ pub const JSValue = enum(JSValueReprInt) {
status,
url,
body,
- data,
+ data
};
// intended to be more lightweight than ZigString
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index dec2d6543..015c740ec 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -1680,6 +1680,28 @@ pub const JSRequest = struct {
return result;
}
+ extern fn RequestPrototype__signalSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void;
+
+ extern fn RequestPrototype__signalGetCachedValue(JSC.JSValue) JSC.JSValue;
+
+ /// `Request.signal` setter
+ /// This value will be visited by the garbage collector.
+ pub fn signalSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ JSC.markBinding(@src());
+ RequestPrototype__signalSetCachedValue(thisValue, globalObject, value);
+ }
+
+ /// `Request.signal` getter
+ /// This value will be visited by the garbage collector.
+ pub fn signalGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
+ JSC.markBinding(@src());
+ const result = RequestPrototype__signalGetCachedValue(thisValue);
+ if (result == .zero)
+ return null;
+
+ return result;
+ }
+
extern fn RequestPrototype__urlSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void;
extern fn RequestPrototype__urlGetCachedValue(JSC.JSValue) JSC.JSValue;
@@ -1799,6 +1821,9 @@ pub const JSRequest = struct {
if (@TypeOf(Request.getReferrerPolicy) != GetterType)
@compileLog("Expected Request.getReferrerPolicy to be a getter");
+ if (@TypeOf(Request.getSignal) != GetterType)
+ @compileLog("Expected Request.getSignal to be a getter");
+
if (@TypeOf(Request.getText) != CallbackType)
@compileLog("Expected Request.getText to be a callback but received " ++ @typeName(@TypeOf(Request.getText)));
if (@TypeOf(Request.getUrl) != GetterType)
@@ -1825,6 +1850,7 @@ pub const JSRequest = struct {
@export(Request.getRedirect, .{ .name = "RequestPrototype__getRedirect" });
@export(Request.getReferrer, .{ .name = "RequestPrototype__getReferrer" });
@export(Request.getReferrerPolicy, .{ .name = "RequestPrototype__getReferrerPolicy" });
+ @export(Request.getSignal, .{ .name = "RequestPrototype__getSignal" });
@export(Request.getText, .{ .name = "RequestPrototype__getText" });
@export(Request.getUrl, .{ .name = "RequestPrototype__getUrl" });
}
diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h
index 45ef8dbb7..3c53d5f99 100644
--- a/src/bun.js/bindings/headers-cpp.h
+++ b/src/bun.js/bindings/headers-cpp.h
@@ -1,4 +1,4 @@
-//-- AUTOGENERATED FILE -- 1676701449
+//-- AUTOGENERATED FILE -- 1676922916
// clang-format off
#pragma once
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index 50c37dde6..6b005216a 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -1,5 +1,5 @@
// clang-format off
-//-- AUTOGENERATED FILE -- 1676945760
+//-- AUTOGENERATED FILE -- 1676922916
#pragma once
#include <stddef.h>
@@ -210,11 +210,14 @@ CPP_DECL JSC__JSInternalPromise* JSC__JSModuleLoader__loadAndEvaluateModule(JSC_
CPP_DECL bool JSC__AbortSignal__aborted(JSC__AbortSignal* arg0);
CPP_DECL JSC__JSValue JSC__AbortSignal__abortReason(JSC__AbortSignal* arg0);
CPP_DECL JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* arg1, void(* ArgFn2)(void* arg0, JSC__JSValue JSValue1)) __attribute__((nonnull (2)));
+CPP_DECL void JSC__AbortSignal__cleanNativeBindings(JSC__AbortSignal* arg0, void* arg1);
+CPP_DECL JSC__JSValue JSC__AbortSignal__create(JSC__JSGlobalObject* arg0);
CPP_DECL JSC__JSValue JSC__AbortSignal__createAbortError(const ZigString* arg0, const ZigString* arg1, JSC__JSGlobalObject* arg2);
CPP_DECL JSC__JSValue JSC__AbortSignal__createTimeoutError(const ZigString* arg0, const ZigString* arg1, JSC__JSGlobalObject* arg2);
CPP_DECL JSC__AbortSignal* JSC__AbortSignal__fromJS(JSC__JSValue JSValue0);
CPP_DECL JSC__AbortSignal* JSC__AbortSignal__ref(JSC__AbortSignal* arg0);
CPP_DECL JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1);
+CPP_DECL JSC__JSValue JSC__AbortSignal__toJS(JSC__AbortSignal* arg0, JSC__JSGlobalObject* arg1);
CPP_DECL JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0);
#pragma mark - JSC::JSPromise
diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig
index 12c72a717..1e0e56dcb 100644
--- a/src/bun.js/bindings/headers.zig
+++ b/src/bun.js/bindings/headers.zig
@@ -138,11 +138,14 @@ pub extern fn JSC__JSModuleLoader__loadAndEvaluateModule(arg0: *bindings.JSGloba
pub extern fn JSC__AbortSignal__aborted(arg0: ?*bindings.AbortSignal) bool;
pub extern fn JSC__AbortSignal__abortReason(arg0: ?*bindings.AbortSignal) JSC__JSValue;
pub extern fn JSC__AbortSignal__addListener(arg0: ?*bindings.AbortSignal, arg1: ?*anyopaque, ArgFn2: ?*const fn (?*anyopaque, JSC__JSValue) callconv(.C) void) ?*bindings.AbortSignal;
+pub extern fn JSC__AbortSignal__cleanNativeBindings(arg0: ?*bindings.AbortSignal, arg1: ?*anyopaque) void;
+pub extern fn JSC__AbortSignal__create(arg0: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__AbortSignal__createAbortError(arg0: [*c]const ZigString, arg1: [*c]const ZigString, arg2: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__AbortSignal__createTimeoutError(arg0: [*c]const ZigString, arg1: [*c]const ZigString, arg2: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__AbortSignal__fromJS(JSValue0: JSC__JSValue) ?*bindings.AbortSignal;
pub extern fn JSC__AbortSignal__ref(arg0: ?*bindings.AbortSignal) ?*bindings.AbortSignal;
pub extern fn JSC__AbortSignal__signal(arg0: ?*bindings.AbortSignal, JSValue1: JSC__JSValue) ?*bindings.AbortSignal;
+pub extern fn JSC__AbortSignal__toJS(arg0: ?*bindings.AbortSignal, arg1: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__AbortSignal__unref(arg0: ?*bindings.AbortSignal) ?*bindings.AbortSignal;
pub extern fn JSC__JSPromise__asValue(arg0: ?*bindings.JSPromise, arg1: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__JSPromise__create(arg0: *bindings.JSGlobalObject) ?*bindings.JSPromise;
diff --git a/src/bun.js/bindings/webcore/AbortSignal.cpp b/src/bun.js/bindings/webcore/AbortSignal.cpp
index 132ecefca..d20af5c81 100644
--- a/src/bun.js/bindings/webcore/AbortSignal.cpp
+++ b/src/bun.js/bindings/webcore/AbortSignal.cpp
@@ -115,6 +115,15 @@ void AbortSignal::signalAbort(JSC::JSValue reason)
dispatchEvent(Event::create(eventNames().abortEvent, Event::CanBubble::No, Event::IsCancelable::No));
}
+void AbortSignal::cleanNativeBindings(void* ref) {
+ auto callbacks = std::exchange(m_native_callbacks, {});
+
+ callbacks.removeAllMatching([=](auto callback){
+ const auto [ ctx, func ] = callback;
+ return ctx == ref;
+ });
+}
+
// https://dom.spec.whatwg.org/#abortsignal-follow
void AbortSignal::signalFollow(AbortSignal& signal)
{
diff --git a/src/bun.js/bindings/webcore/AbortSignal.h b/src/bun.js/bindings/webcore/AbortSignal.h
index b0c59daae..03374b248 100644
--- a/src/bun.js/bindings/webcore/AbortSignal.h
+++ b/src/bun.js/bindings/webcore/AbortSignal.h
@@ -65,6 +65,7 @@ public:
using Algorithm = Function<void(JSValue)>;
void addAlgorithm(Algorithm&& algorithm) { m_algorithms.append(WTFMove(algorithm)); }
+ void cleanNativeBindings(void* ref);
void addNativeCallback(std::tuple<void*, void (*)(void*, JSC::EncodedJSValue)> callback) { m_native_callbacks.append(callback); }
bool isFollowingSignal() const { return !!m_followingSignal; }
diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig
index 04fb3f7e2..b2dfe829e 100644
--- a/src/bun.js/webcore/request.zig
+++ b/src/bun.js/webcore/request.zig
@@ -12,6 +12,7 @@ const js = JSC.C;
const Method = @import("../../http/method.zig").Method;
const FetchHeaders = JSC.FetchHeaders;
+const AbortSignal = JSC.AbortSignal;
const ObjectPool = @import("../../pool.zig").ObjectPool;
const SystemError = JSC.SystemError;
const Output = @import("bun").Output;
@@ -57,6 +58,7 @@ pub const Request = struct {
url_was_allocated: bool = false,
headers: ?*FetchHeaders = null,
+ signal: ?*AbortSignal = null,
body: Body.Value = Body.Value{ .Empty = {} },
method: Method = Method.GET,
uws_request: ?*uws.Request = null,
@@ -217,6 +219,21 @@ pub const Request = struct {
return ZigString.Empty.toValueGC(globalThis);
}
+ pub fn getSignal(this: *Request, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
+ // Already have an C++ instance
+ if(this.signal) |signal| {
+ return signal.toJS(globalThis);
+ } else {
+ //Lazy create default signal
+ const js_signal = AbortSignal.create(globalThis);
+ js_signal.ensureStillAlive();
+ if (AbortSignal.fromJS(js_signal)) |signal| {
+ this.signal = signal;
+ }
+ return js_signal;
+ }
+ }
+
pub fn getMethod(
this: *Request,
globalThis: *JSC.JSGlobalObject,
@@ -254,6 +271,10 @@ pub const Request = struct {
}
this.body.deinit();
+ if(this.signal) |signal| {
+ _ = signal.unref();
+ this.signal = null;
+ }
bun.default_allocator.destroy(this);
}
@@ -415,6 +436,22 @@ pub const Request = struct {
}
}
+ if (arguments[1].get(globalThis, "signal")) |signal_| {
+ if (AbortSignal.fromJS(signal_)) |signal| {
+ //Keep it alive
+ signal_.ensureStillAlive();
+ request.signal = signal;
+ _ = signal.ref();
+
+ } else {
+ if (request.headers) |head| {
+ head.deref();
+ }
+
+ return null;
+ }
+ }
+
request.url = (arguments[0].toSlice(globalThis, bun.default_allocator).cloneIfNeeded(bun.default_allocator) catch {
return null;
}).slice();
@@ -506,6 +543,11 @@ pub const Request = struct {
req.headers = FetchHeaders.createFromUWS(globalThis, uws_req);
this.headers = req.headers.?.cloneThis(globalThis).?;
}
+
+ if(this.signal) |signal| {
+ _ = signal.ref();
+ req.signal = signal;
+ }
}
pub fn clone(this: *Request, allocator: std.mem.Allocator, globalThis: *JSGlobalObject) *Request {
diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts
index 7ebe4774b..67ac4716b 100644
--- a/src/bun.js/webcore/response.classes.ts
+++ b/src/bun.js/webcore/response.classes.ts
@@ -55,6 +55,10 @@ export default [
bodyUsed: {
getter: "getBodyUsed",
},
+ signal: {
+ getter: "getSignal",
+ cache: true,
+ },
},
}),
define({
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
index 5c0cd9c8e..39ee205c2 100644
--- a/src/bun.js/webcore/response.zig
+++ b/src/bun.js/webcore/response.zig
@@ -672,7 +672,7 @@ pub const Fetch = struct {
true => this.onResolve(),
false => this.onReject(),
};
-
+ result.ensureStillAlive();
this.clearData();
promise_value.ensureStillAlive();
@@ -1015,6 +1015,10 @@ pub const Fetch = struct {
// no proxy only url
url = ZigURL.parse(getAllocator(ctx).dupe(u8, request.url) catch unreachable);
url_proxy_buffer = url.href;
+ if (request.signal) |signal_| {
+ _ = signal_.ref();
+ signal = signal_;
+ }
}
} else if (first_arg.toStringOrNull(globalThis)) |jsstring| {
if (arguments.len >= 2) {
diff --git a/src/http_client_async.zig b/src/http_client_async.zig
index b6dd0e828..0c7395319 100644
--- a/src/http_client_async.zig
+++ b/src/http_client_async.zig
@@ -235,6 +235,8 @@ fn NewHTTPContext(comptime ssl: bool) type {
/// Attempt to keep the socket alive by reusing it for another request.
/// If no space is available, close the socket.
pub fn releaseSocket(this: *@This(), socket: HTTPSocket, hostname: []const u8, port: u16) void {
+ log("releaseSocket", .{});
+
if (comptime Environment.allow_assert) {
std.debug.assert(!socket.isClosed());
std.debug.assert(!socket.isShutdown());
@@ -1015,7 +1017,9 @@ pub fn ClientSocketAbortHandler(comptime is_ssl: bool) type {
log("onAborted", .{});
if (this) |this_| {
const self = bun.cast(*@This(), this_);
- self.client.closeAndAbort(reason, is_ssl, self.socket);
+ if (self.client.state.response_stage != .done and self.client.state.response_stage != .fail) {
+ self.client.closeAndAbort(reason, is_ssl, self.socket);
+ }
}
}
@@ -1031,6 +1035,14 @@ pub fn ClientSocketAbortHandler(comptime is_ssl: bool) type {
pub fn addAbortSignalEventListenner(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
if (this.signal) |signal| {
+ const aborted = signal.aborted();
+ if (aborted) {
+ log("addAbortSignalEventListenner already aborted!", .{});
+ const reason = signal.abortReason();
+ this.closeAndAbort(reason, is_ssl, socket);
+ return;
+ }
+
const handler = ClientSocketAbortHandler(is_ssl).init(this, socket) catch unreachable;
this.abort_handler = bun.cast(*anyopaque, handler);
this.abort_handler_deinit = ClientSocketAbortHandler(is_ssl).deinit;
@@ -1054,6 +1066,15 @@ pub fn hasSignalAborted(this: *HTTPClient) ?JSC.JSValue {
return null;
}
+pub fn deinitSignal(this: *HTTPClient) void {
+ if (this.signal != null) {
+ var signal = this.signal.?;
+ const ctx = bun.cast(*anyopaque, this);
+ signal.cleanNativeBindings(ctx);
+ _ = signal.unref();
+ this.signal = null;
+ }
+}
pub fn deinit(this: *HTTPClient) void {
if (this.redirect) |redirect| {
redirect.release();
@@ -1068,11 +1089,8 @@ pub fn deinit(this: *HTTPClient) void {
this.proxy_tunnel = null;
}
- if (this.signal != null) {
- var signal = this.signal.?;
- _ = signal.unref();
- this.signal = null;
- }
+ this.deinitSignal();
+
if (this.abort_handler != null and this.abort_handler_deinit != null) {
this.abort_handler_deinit.?(this.abort_handler.?);
this.abort_handler = null;
@@ -1538,6 +1556,12 @@ pub fn start(this: *HTTPClient, body: []const u8, body_out_str: *MutableString)
}
fn start_(this: *HTTPClient, comptime is_ssl: bool) void {
+ // Aborted before connecting
+ if (this.hasSignalAborted()) |reason| {
+ this.fail(error.Aborted, reason);
+ return;
+ }
+
var socket = http_thread.connect(this, is_ssl) catch |err| {
this.fail(err, null);
return;
@@ -1815,12 +1839,17 @@ pub fn onWritable(this: *HTTPClient, comptime is_first_call: bool, comptime is_s
}
pub fn closeAndFail(this: *HTTPClient, err: anyerror, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
- socket.ext(**anyopaque).?.* = bun.cast(
- **anyopaque,
- NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(),
- );
- socket.close(0, null);
- this.fail(err, null);
+ if (this.state.stage != .fail and this.state.stage != .done) {
+ log("closeAndFail", .{});
+ if (!socket.isClosed()) {
+ socket.ext(**anyopaque).?.* = bun.cast(
+ **anyopaque,
+ NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(),
+ );
+ socket.close(0, null);
+ }
+ this.fail(err, null);
+ }
}
fn startProxySendHeaders(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
@@ -2112,12 +2141,17 @@ pub fn onData(this: *HTTPClient, comptime is_ssl: bool, incoming_data: []const u
}
pub fn closeAndAbort(this: *HTTPClient, reason: JSC.JSValue, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
- socket.ext(**anyopaque).?.* = bun.cast(
- **anyopaque,
- NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(),
- );
- socket.close(0, null);
- this.fail(error.Aborted, reason);
+ if (this.state.stage != .fail and this.state.stage != .done) {
+ log("closeAndAbort", .{});
+ if (!socket.isClosed()) {
+ socket.ext(**anyopaque).?.* = bun.cast(
+ **anyopaque,
+ NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr(),
+ );
+ socket.close(0, null);
+ }
+ this.fail(error.Aborted, reason);
+ }
}
fn fail(this: *HTTPClient, err: anyerror, reason: ?JSC.JSValue) void {
@@ -2132,6 +2166,8 @@ fn fail(this: *HTTPClient, err: anyerror, reason: ?JSC.JSValue) void {
this.proxy_tunneling = false;
callback.run(result);
+
+ this.deinitSignal();
}
// We have to clone metadata immediately after use
@@ -2164,45 +2200,47 @@ pub fn setTimeout(this: *HTTPClient, socket: anytype, amount: c_uint) void {
}
pub fn done(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPContext(is_ssl), socket: NewHTTPContext(is_ssl).HTTPSocket) void {
- var out_str = this.state.body_out_str.?;
- var body = out_str.*;
- this.cloned_metadata.response = this.state.pending_response;
- const result = this.toResult(this.cloned_metadata, null);
- const callback = this.completion_callback;
+ if (this.state.stage != .done and this.state.stage != .fail) {
+ log("done", .{});
- this.state.response_stage = .done;
- this.state.request_stage = .done;
- this.state.stage = .done;
+ this.deinitSignal();
- socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr());
+ var out_str = this.state.body_out_str.?;
+ var body = out_str.*;
+ this.cloned_metadata.response = this.state.pending_response;
+ const result = this.toResult(this.cloned_metadata, null);
+ const callback = this.completion_callback;
- if (this.state.allow_keepalive and !this.disable_keepalive and !socket.isClosed() and FeatureFlags.enable_keepalive) {
- ctx.releaseSocket(
- socket,
- this.connected_url.hostname,
- this.connected_url.getPortAuto(),
- );
- } else if (!socket.isClosed()) {
- socket.close(0, null);
- }
+ socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, NewHTTPContext(is_ssl).ActiveSocket.init(&dead_socket).ptr());
- this.state.reset();
- result.body.?.* = body;
- std.debug.assert(this.state.stage != .done);
- this.state.response_stage = .done;
- this.state.request_stage = .done;
- this.state.stage = .done;
- this.proxy_tunneling = false;
- if (comptime print_every > 0) {
- print_every_i += 1;
- if (print_every_i % print_every == 0) {
- Output.prettyln("Heap stats for HTTP thread\n", .{});
- Output.flush();
- default_arena.dumpThreadStats();
- print_every_i = 0;
+ if (this.state.allow_keepalive and !this.disable_keepalive and !socket.isClosed() and FeatureFlags.enable_keepalive) {
+ ctx.releaseSocket(
+ socket,
+ this.connected_url.hostname,
+ this.connected_url.getPortAuto(),
+ );
+ } else if (!socket.isClosed()) {
+ socket.close(0, null);
+ }
+
+ this.state.reset();
+ result.body.?.* = body;
+ std.debug.assert(this.state.stage != .done);
+ this.state.response_stage = .done;
+ this.state.request_stage = .done;
+ this.state.stage = .done;
+ this.proxy_tunneling = false;
+ if (comptime print_every > 0) {
+ print_every_i += 1;
+ if (print_every_i % print_every == 0) {
+ Output.prettyln("Heap stats for HTTP thread\n", .{});
+ Output.flush();
+ default_arena.dumpThreadStats();
+ print_every_i = 0;
+ }
}
+ callback.run(result);
}
- callback.run(result);
}
pub const HTTPClientResult = struct {
diff --git a/test/bun.js/bun-server.test.ts b/test/bun.js/bun-server.test.ts
index 667d7bdca..98554dfbb 100644
--- a/test/bun.js/bun-server.test.ts
+++ b/test/bun.js/bun-server.test.ts
@@ -26,4 +26,28 @@ describe("Server", () => {
expect(await response.text()).toBe("Hello");
server.stop();
});
+
+ test('abort signal on server', async ()=> {
+ {
+ let signalOnServer = false;
+ const server = Bun.serve({
+ async fetch(req) {
+ req.signal.addEventListener("abort", () => {
+ signalOnServer = true;
+ });
+ await Bun.sleep(3000);
+ return new Response("Hello");
+ },
+ port: 54321,
+ });
+
+ try {
+ await fetch("http://localhost:54321", { signal: AbortSignal.timeout(100) });
+ } catch {}
+ await Bun.sleep(300);
+ expect(signalOnServer).toBe(true);
+ server.stop();
+ }
+
+ })
});
diff --git a/test/bun.js/fetch.test.js b/test/bun.js/fetch.test.js
index 46409633c..0e222b984 100644
--- a/test/bun.js/fetch.test.js
+++ b/test/bun.js/fetch.test.js
@@ -8,64 +8,289 @@ const exampleFixture = fs.readFileSync(
"utf8",
);
-let server ;
+let server;
beforeAll(() => {
server = Bun.serve({
- async fetch(){
+ async fetch(request) {
+
+ if (request.url.endsWith("/nodelay")) {
+ return new Response("Hello")
+ }
+ if (request.url.endsWith("/stream")) {
+ const reader = request.body.getReader();
+ const body = new ReadableStream({
+ async pull(controller) {
+ if (!reader) controller.close();
+ const { done, value } = await reader.read();
+ // When no more data needs to be consumed, close the stream
+ if (done) {
+ controller.close();
+ return;
+ }
+ // Enqueue the next data chunk into our target stream
+ controller.enqueue(value);
+ }
+ });
+ return new Response(body);
+ }
+ if ((request.method).toUpperCase() === "POST") {
+ const body = await request.text();
+ return new Response(body);
+ }
await Bun.sleep(2000);
return new Response("Hello")
},
port: 64321
});
-
+
});
afterAll(() => {
server.stop();
});
+const payload = new Uint8Array(1024 * 1024 * 2);
+crypto.getRandomValues(payload);
+
+describe("AbortSignalStreamTest",async () => {
+
+ const port = 84322;
+ async function abortOnStage(body, stage, port) {
+ let error = undefined;
+ var abortController = new AbortController();
+ {
+ const server = Bun.serve({
+ async fetch(request) {
+ let chunk_count = 0;
+ const reader = request.body.getReader();
+ return Response(
+ new ReadableStream({
+ async pull(controller) {
+ while (true) {
+ chunk_count++;
+
+ const { done, value } = await reader.read();
+ if (chunk_count == stage) {
+ abortController.abort();
+ }
+
+ if (done) {
+ controller.close();
+ return;
+ }
+ controller.enqueue(value);
+ }
+ },
+ }),
+ );
+ },
+ port,
+ });
-describe("AbortSignal", ()=> {
- it("AbortError", async ()=> {
+ try {
+ const signal = abortController.signal;
+
+ await fetch(`http://127.0.0.1:${port}`, { method: "POST", body, signal: signal }).then((res) => res.arrayBuffer());
+
+ } catch (ex) {
+ error = ex;
+
+ }
+ server.stop();
+ expect(error instanceof DOMException).toBeTruthy();
+ expect(error.name).toBe("AbortError");
+ expect(error.message).toBe("The operation was aborted.");
+ }
+ }
+
+ for (let i = 1; i < 7; i++) {
+ await it(`Abort after ${i} chunks`, async () => {
+ await abortOnStage(payload, i, port + i);
+ })();
+ }
+})
+
+describe("AbortSignalDirectStreamTest", () => {
+ const port = 74322;
+ async function abortOnStage(body, stage, port) {
+ let error = undefined;
+ var abortController = new AbortController();
+ {
+ const server = Bun.serve({
+ async fetch(request) {
+ let chunk_count = 0;
+ const reader = request.body.getReader();
+ return Response(
+ new ReadableStream({
+ type: "direct",
+ async pull(controller) {
+ while (true) {
+ chunk_count++;
+
+ const { done, value } = await reader.read();
+ if (chunk_count == stage) {
+ abortController.abort();
+ }
+
+ if (done) {
+ controller.end();
+ return;
+ }
+ controller.write(value);
+ }
+ },
+ }),
+ );
+ },
+ port,
+ });
+
+ try {
+ const signal = abortController.signal;
+
+ await fetch(`http://127.0.0.1:${port}`, { method: "POST", body, signal: signal }).then((res) => res.arrayBuffer());
+
+ } catch (ex) {
+ error = ex;
+ }
+ server.stop();
+ expect(error instanceof DOMException).toBeTruthy();
+ expect(error.name).toBe("AbortError");
+ expect(error.message).toBe("The operation was aborted.");
+ }
+ }
+
+ for (let i = 1; i < 7; i++) {
+ await it(`Abort after ${i} chunks`, async () => {
+ await abortOnStage(payload, i, port + i);
+ })();
+ }
+})
+
+describe("AbortSignal", () => {
+ it("AbortError", async () => {
let name;
try {
var controller = new AbortController();
const signal = controller.signal;
- async function manualAbort(){
+ async function manualAbort() {
await Bun.sleep(10);
controller.abort();
}
- await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text()), manualAbort()]);
- } catch (error){
+ await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]);
+ } catch (error) {
name = error.name;
}
expect(name).toBe("AbortError");
})
- it("AbortErrorWithReason", async ()=> {
+
+ it("AbortAfterFinish", async () => {
+ let error = undefined;
+ try {
+ var controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch("http://127.0.0.1:64321/nodelay", { signal: signal }).then((res) => res.text())
+ controller.abort();
+ } catch (ex) {
+ error = ex;
+ }
+ expect(error).toBeUndefined();
+ })
+
+ it("AbortErrorWithReason", async () => {
let reason;
try {
var controller = new AbortController();
const signal = controller.signal;
- async function manualAbort(){
+ async function manualAbort() {
await Bun.sleep(10);
controller.abort("My Reason");
}
- await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text()), manualAbort()]);
- } catch (error){
- reason = error
+ await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]);
+ } catch (error) {
+ reason = error
}
expect(reason).toBe("My Reason");
})
- it("TimeoutError", async ()=> {
+
+ it("AbortErrorEventListener", async () => {
+ let name;
+ try {
+ var controller = new AbortController();
+ const signal = controller.signal;
+ var eventSignal = undefined;
+ signal.addEventListener("abort", (ev) => {
+ eventSignal = ev.currentTarget;
+ });
+
+ async function manualAbort() {
+ await Bun.sleep(10);
+ controller.abort();
+ }
+ await Promise.all([fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text()), manualAbort()]);
+ } catch (error) {
+ name = error.name;
+ }
+ expect(eventSignal).toBeDefined();
+ expect(eventSignal.reason.name).toBe(name);
+ expect(eventSignal.aborted).toBe(true);
+ })
+
+ it("AbortErrorWhileUploading", async () => {
+ const abortController = new AbortController();
+ let error;
+ try {
+ await fetch(
+ "http://localhost:64321",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ //this will abort immediately should abort before connected
+ abortController.abort();
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ } catch (ex) {
+ error = ex
+ }
+
+ expect(error instanceof DOMException).toBeTruthy();
+ expect(error.name).toBe("AbortError");
+ expect(error.message).toBe("The operation was aborted.");
+ });
+
+ it("TimeoutError", async () => {
let name;
try {
const signal = AbortSignal.timeout(10);
- await fetch("http://127.0.0.1:64321", { signal: signal }).then((res)=> res.text());
- } catch (error){
+ await fetch("http://127.0.0.1:64321", { signal: signal }).then((res) => res.text());
+ } catch (error) {
name = error.name;
}
expect(name).toBe("TimeoutError");
})
+ it("Request", async () => {
+ let name;
+ try {
+ var controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("http://127.0.0.1:64321", { signal });
+ async function manualAbort() {
+ await Bun.sleep(10);
+ controller.abort();
+ }
+ await Promise.all([fetch(request).then((res) => res.text()), manualAbort()]);
+ } catch (error) {
+ name = error.name
+ }
+ expect(name).toBe("AbortError");
+ })
+
})
describe("Headers", () => {
@@ -296,33 +521,31 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) {
if (withGC) gc();
});
- it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${
- withGC ? " (with gc) " : ""
- }`, async () => {
- if (withGC) gc();
- var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
- if (withGC) gc();
- expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
- if (withGC) gc();
- });
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
- it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${
- withGC ? " (with gc) " : ""
- }`, async () => {
- if (withGC) gc();
- var response = blobbyConstructor(
- new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"),
- );
- if (withGC) gc();
- var failed = false;
- try {
- await response.json();
- } catch (e) {
- failed = true;
- }
- expect(failed).toBe(true);
- if (withGC) gc();
- });
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(
+ new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"),
+ );
+ if (withGC) gc();
+ var failed = false;
+ try {
+ await response.json();
+ } catch (e) {
+ failed = true;
+ }
+ expect(failed).toBe(true);
+ if (withGC) gc();
+ });
it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
@@ -332,15 +555,14 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) {
if (withGC) gc();
});
- it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${
- withGC ? " (with gc) " : ""
- }`, async () => {
- if (withGC) gc();
- var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
- if (withGC) gc();
- expect(await response.text()).toBe(JSON.stringify(jsonObject));
- if (withGC) gc();
- });
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(await response.text()).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
@@ -365,30 +587,29 @@ function testBlobInterface(blobbyConstructor, hasBlobFn) {
if (withGC) gc();
});
- it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${
- withGC ? " (with gc) " : ""
- }`, async () => {
- if (withGC) gc();
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
- var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
- if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
- const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
- if (withGC) gc();
+ const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
+ if (withGC) gc();
- const compare = new Uint8Array(await response.arrayBuffer());
- if (withGC) gc();
+ const compare = new Uint8Array(await response.arrayBuffer());
+ if (withGC) gc();
- withoutAggressiveGC(() => {
- for (let i = 0; i < compare.length; i++) {
- if (withGC) gc();
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < compare.length; i++) {
+ if (withGC) gc();
- expect(compare[i]).toBe(bytes[i]);
- if (withGC) gc();
- }
+ expect(compare[i]).toBe(bytes[i]);
+ if (withGC) gc();
+ }
+ });
+ if (withGC) gc();
});
- if (withGC) gc();
- });
hasBlobFn &&
it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${withGC ? " (with gc) " : ""}`, async () => {
@@ -445,7 +666,7 @@ describe("Bun.file", () => {
it("size is Infinity on a fifo", () => {
try {
unlinkSync("/tmp/test-fifo");
- } catch (e) {}
+ } catch (e) { }
mkfifo("/tmp/test-fifo");
const { size } = Bun.file("/tmp/test-fifo");
@@ -463,14 +684,14 @@ describe("Bun.file", () => {
beforeAll(async () => {
try {
unlinkSync("/tmp/my-new-file");
- } catch {}
+ } catch { }
await Bun.write("/tmp/my-new-file", "hey");
chmodSync("/tmp/my-new-file", 0o000);
});
afterAll(() => {
try {
unlinkSync("/tmp/my-new-file");
- } catch {}
+ } catch { }
});
forEachMethod(m => () => {
@@ -483,7 +704,7 @@ describe("Bun.file", () => {
beforeAll(() => {
try {
unlinkSync("/tmp/does-not-exist");
- } catch {}
+ } catch { }
});
forEachMethod(m => async () => {
@@ -732,10 +953,14 @@ describe("Request", () => {
body: "<div>hello</div>",
});
gc();
+ expect(body.signal).toBeDefined();
+ gc();
expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8");
gc();
var clone = body.clone();
gc();
+ expect(clone.signal).toBeDefined();
+ gc();
body.headers.set("content-type", "text/plain");
gc();
expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
@@ -743,9 +968,33 @@ describe("Request", () => {
expect(body.headers.get("content-type")).toBe("text/plain");
gc();
expect(await clone.text()).toBe("<div>hello</div>");
- gc();
});
+ it("signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal })
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ })
+
+ it("cloned signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal })
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ gc();
+ const cloned = req.clone();
+ expect(cloned.signal.aborted).toBe(true);
+ })
+
testBlobInterface(data => new Request("https://hello.com", { body: data }), true);
});