aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/schema.js632
-rw-r--r--src/bun.js/api/server.zig14
-rw-r--r--src/deps/libuwsockets.cpp20
-rw-r--r--test/regression/issue/02499-repro.ts21
-rw-r--r--test/regression/issue/02499.test.ts95
5 files changed, 462 insertions, 320 deletions
diff --git a/src/api/schema.js b/src/api/schema.js
index fde187ce0..cc25603b3 100644
--- a/src/api/schema.js
+++ b/src/api/schema.js
@@ -1,90 +1,90 @@
const Loader = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- 7: 7,
- 8: 8,
- 9: 9,
- 10: 10,
- jsx: 1,
- js: 2,
- ts: 3,
- tsx: 4,
- css: 5,
- file: 6,
- json: 7,
- toml: 8,
- wasm: 9,
- napi: 10,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "7": 7,
+ "8": 8,
+ "9": 9,
+ "10": 10,
+ "jsx": 1,
+ "js": 2,
+ "ts": 3,
+ "tsx": 4,
+ "css": 5,
+ "file": 6,
+ "json": 7,
+ "toml": 8,
+ "wasm": 9,
+ "napi": 10,
};
const LoaderKeys = {
- 1: "jsx",
- 2: "js",
- 3: "ts",
- 4: "tsx",
- 5: "css",
- 6: "file",
- 7: "json",
- 8: "toml",
- 9: "wasm",
- 10: "napi",
- jsx: "jsx",
- js: "js",
- ts: "ts",
- tsx: "tsx",
- css: "css",
- file: "file",
- json: "json",
- toml: "toml",
- wasm: "wasm",
- napi: "napi",
+ "1": "jsx",
+ "2": "js",
+ "3": "ts",
+ "4": "tsx",
+ "5": "css",
+ "6": "file",
+ "7": "json",
+ "8": "toml",
+ "9": "wasm",
+ "10": "napi",
+ "jsx": "jsx",
+ "js": "js",
+ "ts": "ts",
+ "tsx": "tsx",
+ "css": "css",
+ "file": "file",
+ "json": "json",
+ "toml": "toml",
+ "wasm": "wasm",
+ "napi": "napi",
};
const FrameworkEntryPointType = {
- 1: 1,
- 2: 2,
- 3: 3,
- client: 1,
- server: 2,
- fallback: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "client": 1,
+ "server": 2,
+ "fallback": 3,
};
const FrameworkEntryPointTypeKeys = {
- 1: "client",
- 2: "server",
- 3: "fallback",
- client: "client",
- server: "server",
- fallback: "fallback",
+ "1": "client",
+ "2": "server",
+ "3": "fallback",
+ "client": "client",
+ "server": "server",
+ "fallback": "fallback",
};
const StackFrameScope = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- Eval: 1,
- Module: 2,
- Function: 3,
- Global: 4,
- Wasm: 5,
- Constructor: 6,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "Eval": 1,
+ "Module": 2,
+ "Function": 3,
+ "Global": 4,
+ "Wasm": 5,
+ "Constructor": 6,
};
const StackFrameScopeKeys = {
- 1: "Eval",
- 2: "Module",
- 3: "Function",
- 4: "Global",
- 5: "Wasm",
- 6: "Constructor",
- Eval: "Eval",
- Module: "Module",
- Function: "Function",
- Global: "Global",
- Wasm: "Wasm",
- Constructor: "Constructor",
+ "1": "Eval",
+ "2": "Module",
+ "3": "Function",
+ "4": "Global",
+ "5": "Wasm",
+ "6": "Constructor",
+ "Eval": "Eval",
+ "Module": "Module",
+ "Function": "Function",
+ "Global": "Global",
+ "Wasm": "Wasm",
+ "Constructor": "Constructor",
};
function decodeStackFrame(bb) {
@@ -332,40 +332,40 @@ function encodeJSException(message, bb) {
bb.writeByte(0);
}
const FallbackStep = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- 7: 7,
- 8: 8,
- ssr_disabled: 1,
- create_vm: 2,
- configure_router: 3,
- configure_defines: 4,
- resolve_entry_point: 5,
- load_entry_point: 6,
- eval_entry_point: 7,
- fetch_event_handler: 8,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "7": 7,
+ "8": 8,
+ "ssr_disabled": 1,
+ "create_vm": 2,
+ "configure_router": 3,
+ "configure_defines": 4,
+ "resolve_entry_point": 5,
+ "load_entry_point": 6,
+ "eval_entry_point": 7,
+ "fetch_event_handler": 8,
};
const FallbackStepKeys = {
- 1: "ssr_disabled",
- 2: "create_vm",
- 3: "configure_router",
- 4: "configure_defines",
- 5: "resolve_entry_point",
- 6: "load_entry_point",
- 7: "eval_entry_point",
- 8: "fetch_event_handler",
- ssr_disabled: "ssr_disabled",
- create_vm: "create_vm",
- configure_router: "configure_router",
- configure_defines: "configure_defines",
- resolve_entry_point: "resolve_entry_point",
- load_entry_point: "load_entry_point",
- eval_entry_point: "eval_entry_point",
- fetch_event_handler: "fetch_event_handler",
+ "1": "ssr_disabled",
+ "2": "create_vm",
+ "3": "configure_router",
+ "4": "configure_defines",
+ "5": "resolve_entry_point",
+ "6": "load_entry_point",
+ "7": "eval_entry_point",
+ "8": "fetch_event_handler",
+ "ssr_disabled": "ssr_disabled",
+ "create_vm": "create_vm",
+ "configure_router": "configure_router",
+ "configure_defines": "configure_defines",
+ "resolve_entry_point": "resolve_entry_point",
+ "load_entry_point": "load_entry_point",
+ "eval_entry_point": "eval_entry_point",
+ "fetch_event_handler": "fetch_event_handler",
};
function decodeProblems(bb) {
@@ -517,76 +517,76 @@ function encodeFallbackMessageContainer(message, bb) {
bb.writeByte(0);
}
const ResolveMode = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- disable: 1,
- lazy: 2,
- dev: 3,
- bundle: 4,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "disable": 1,
+ "lazy": 2,
+ "dev": 3,
+ "bundle": 4,
};
const ResolveModeKeys = {
- 1: "disable",
- 2: "lazy",
- 3: "dev",
- 4: "bundle",
- disable: "disable",
- lazy: "lazy",
- dev: "dev",
- bundle: "bundle",
+ "1": "disable",
+ "2": "lazy",
+ "3": "dev",
+ "4": "bundle",
+ "disable": "disable",
+ "lazy": "lazy",
+ "dev": "dev",
+ "bundle": "bundle",
};
const Platform = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- browser: 1,
- node: 2,
- bun: 3,
- bun_macro: 4,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "browser": 1,
+ "node": 2,
+ "bun": 3,
+ "bun_macro": 4,
};
const PlatformKeys = {
- 1: "browser",
- 2: "node",
- 3: "bun",
- 4: "bun_macro",
- browser: "browser",
- node: "node",
- bun: "bun",
- bun_macro: "bun_macro",
+ "1": "browser",
+ "2": "node",
+ "3": "bun",
+ "4": "bun_macro",
+ "browser": "browser",
+ "node": "node",
+ "bun": "bun",
+ "bun_macro": "bun_macro",
};
const CSSInJSBehavior = {
- 1: 1,
- 2: 2,
- 3: 3,
- facade: 1,
- facade_onimportcss: 2,
- auto_onimportcss: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "facade": 1,
+ "facade_onimportcss": 2,
+ "auto_onimportcss": 3,
};
const CSSInJSBehaviorKeys = {
- 1: "facade",
- 2: "facade_onimportcss",
- 3: "auto_onimportcss",
- facade: "facade",
- facade_onimportcss: "facade_onimportcss",
- auto_onimportcss: "auto_onimportcss",
+ "1": "facade",
+ "2": "facade_onimportcss",
+ "3": "auto_onimportcss",
+ "facade": "facade",
+ "facade_onimportcss": "facade_onimportcss",
+ "auto_onimportcss": "auto_onimportcss",
};
const JSXRuntime = {
- 1: 1,
- 2: 2,
- 3: 3,
- automatic: 1,
- classic: 2,
- solid: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "automatic": 1,
+ "classic": 2,
+ "solid": 3,
};
const JSXRuntimeKeys = {
- 1: "automatic",
- 2: "classic",
- 3: "solid",
- automatic: "automatic",
- classic: "classic",
- solid: "solid",
+ "1": "automatic",
+ "2": "classic",
+ "3": "solid",
+ "automatic": "automatic",
+ "classic": "classic",
+ "solid": "solid",
};
function decodeJSX(bb) {
@@ -914,28 +914,28 @@ function encodeJavascriptBundleContainer(message, bb) {
bb.writeByte(0);
}
const ScanDependencyMode = {
- 1: 1,
- 2: 2,
- app: 1,
- all: 2,
+ "1": 1,
+ "2": 2,
+ "app": 1,
+ "all": 2,
};
const ScanDependencyModeKeys = {
- 1: "app",
- 2: "all",
- app: "app",
- all: "all",
+ "1": "app",
+ "2": "all",
+ "app": "app",
+ "all": "all",
};
const ModuleImportType = {
- 1: 1,
- 2: 2,
- import: 1,
- require: 2,
+ "1": 1,
+ "2": 2,
+ "import": 1,
+ "require": 2,
};
const ModuleImportTypeKeys = {
- 1: "import",
- 2: "require",
- import: "import",
- require: "require",
+ "1": "import",
+ "2": "require",
+ "import": "import",
+ "require": "require",
};
function decodeModuleImportRecord(bb) {
@@ -1086,20 +1086,20 @@ function encodeLoaderMap(message, bb) {
}
}
const DotEnvBehavior = {
- 1: 1,
- 2: 2,
- 3: 3,
- disable: 1,
- prefix: 2,
- load_all: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "disable": 1,
+ "prefix": 2,
+ "load_all": 3,
};
const DotEnvBehaviorKeys = {
- 1: "disable",
- 2: "prefix",
- 3: "load_all",
- disable: "disable",
- prefix: "prefix",
- load_all: "load_all",
+ "1": "disable",
+ "2": "prefix",
+ "3": "load_all",
+ "disable": "disable",
+ "prefix": "prefix",
+ "load_all": "load_all",
};
function decodeEnvConfig(bb) {
@@ -1905,16 +1905,16 @@ function encodeTransformOptions(message, bb) {
bb.writeByte(0);
}
const SourceMapMode = {
- 1: 1,
- 2: 2,
- inline_into_file: 1,
- external: 2,
+ "1": 1,
+ "2": 2,
+ "inline_into_file": 1,
+ "external": 2,
};
const SourceMapModeKeys = {
- 1: "inline_into_file",
- 2: "external",
- inline_into_file: "inline_into_file",
- external: "external",
+ "1": "inline_into_file",
+ "2": "external",
+ "inline_into_file": "inline_into_file",
+ "external": "external",
};
function decodeFileHandle(bb) {
@@ -2133,52 +2133,52 @@ function encodeScannedImport(message, bb) {
}
}
const ImportKind = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- 7: 7,
- 8: 8,
- entry_point: 1,
- stmt: 2,
- require: 3,
- dynamic: 4,
- require_resolve: 5,
- at: 6,
- url: 7,
- internal: 8,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "7": 7,
+ "8": 8,
+ "entry_point": 1,
+ "stmt": 2,
+ "require": 3,
+ "dynamic": 4,
+ "require_resolve": 5,
+ "at": 6,
+ "url": 7,
+ "internal": 8,
};
const ImportKindKeys = {
- 1: "entry_point",
- 2: "stmt",
- 3: "require",
- 4: "dynamic",
- 5: "require_resolve",
- 6: "at",
- 7: "url",
- 8: "internal",
- entry_point: "entry_point",
- stmt: "stmt",
- require: "require",
- dynamic: "dynamic",
- require_resolve: "require_resolve",
- at: "at",
- url: "url",
- internal: "internal",
+ "1": "entry_point",
+ "2": "stmt",
+ "3": "require",
+ "4": "dynamic",
+ "5": "require_resolve",
+ "6": "at",
+ "7": "url",
+ "8": "internal",
+ "entry_point": "entry_point",
+ "stmt": "stmt",
+ "require": "require",
+ "dynamic": "dynamic",
+ "require_resolve": "require_resolve",
+ "at": "at",
+ "url": "url",
+ "internal": "internal",
};
const TransformResponseStatus = {
- 1: 1,
- 2: 2,
- success: 1,
- fail: 2,
+ "1": 1,
+ "2": 2,
+ "success": 1,
+ "fail": 2,
};
const TransformResponseStatusKeys = {
- 1: "success",
- 2: "fail",
- success: "success",
- fail: "fail",
+ "1": "success",
+ "2": "fail",
+ "success": "success",
+ "fail": "fail",
};
function decodeOutputFile(bb) {
@@ -2256,28 +2256,28 @@ function encodeTransformResponse(message, bb) {
}
}
const MessageLevel = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- err: 1,
- warn: 2,
- note: 3,
- info: 4,
- debug: 5,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "err": 1,
+ "warn": 2,
+ "note": 3,
+ "info": 4,
+ "debug": 5,
};
const MessageLevelKeys = {
- 1: "err",
- 2: "warn",
- 3: "note",
- 4: "info",
- 5: "debug",
- err: "err",
- warn: "warn",
- note: "note",
- info: "info",
- debug: "debug",
+ "1": "err",
+ "2": "warn",
+ "3": "note",
+ "4": "info",
+ "5": "debug",
+ "err": "err",
+ "warn": "warn",
+ "note": "note",
+ "info": "info",
+ "debug": "debug",
};
function decodeLocation(bb) {
@@ -2508,72 +2508,72 @@ function encodeLog(message, bb) {
}
}
const Reloader = {
- 1: 1,
- 2: 2,
- 3: 3,
- disable: 1,
- live: 2,
- fast_refresh: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "disable": 1,
+ "live": 2,
+ "fast_refresh": 3,
};
const ReloaderKeys = {
- 1: "disable",
- 2: "live",
- 3: "fast_refresh",
- disable: "disable",
- live: "live",
- fast_refresh: "fast_refresh",
+ "1": "disable",
+ "2": "live",
+ "3": "fast_refresh",
+ "disable": "disable",
+ "live": "live",
+ "fast_refresh": "fast_refresh",
};
const WebsocketMessageKind = {
- 1: 1,
- 2: 2,
- 3: 3,
- 4: 4,
- 5: 5,
- 6: 6,
- 7: 7,
- 8: 8,
- welcome: 1,
- file_change_notification: 2,
- build_success: 3,
- build_fail: 4,
- manifest_success: 5,
- manifest_fail: 6,
- resolve_file: 7,
- file_change_notification_with_hint: 8,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "4": 4,
+ "5": 5,
+ "6": 6,
+ "7": 7,
+ "8": 8,
+ "welcome": 1,
+ "file_change_notification": 2,
+ "build_success": 3,
+ "build_fail": 4,
+ "manifest_success": 5,
+ "manifest_fail": 6,
+ "resolve_file": 7,
+ "file_change_notification_with_hint": 8,
};
const WebsocketMessageKindKeys = {
- 1: "welcome",
- 2: "file_change_notification",
- 3: "build_success",
- 4: "build_fail",
- 5: "manifest_success",
- 6: "manifest_fail",
- 7: "resolve_file",
- 8: "file_change_notification_with_hint",
- welcome: "welcome",
- file_change_notification: "file_change_notification",
- build_success: "build_success",
- build_fail: "build_fail",
- manifest_success: "manifest_success",
- manifest_fail: "manifest_fail",
- resolve_file: "resolve_file",
- file_change_notification_with_hint: "file_change_notification_with_hint",
+ "1": "welcome",
+ "2": "file_change_notification",
+ "3": "build_success",
+ "4": "build_fail",
+ "5": "manifest_success",
+ "6": "manifest_fail",
+ "7": "resolve_file",
+ "8": "file_change_notification_with_hint",
+ "welcome": "welcome",
+ "file_change_notification": "file_change_notification",
+ "build_success": "build_success",
+ "build_fail": "build_fail",
+ "manifest_success": "manifest_success",
+ "manifest_fail": "manifest_fail",
+ "resolve_file": "resolve_file",
+ "file_change_notification_with_hint": "file_change_notification_with_hint",
};
const WebsocketCommandKind = {
- 1: 1,
- 2: 2,
- 3: 3,
- build: 1,
- manifest: 2,
- build_with_file_path: 3,
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "build": 1,
+ "manifest": 2,
+ "build_with_file_path": 3,
};
const WebsocketCommandKindKeys = {
- 1: "build",
- 2: "manifest",
- 3: "build_with_file_path",
- build: "build",
- manifest: "manifest",
- build_with_file_path: "build_with_file_path",
+ "1": "build",
+ "2": "manifest",
+ "3": "build_with_file_path",
+ "build": "build",
+ "manifest": "manifest",
+ "build_with_file_path": "build_with_file_path",
};
function decodeWebsocketMessage(bb) {
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig
index 3cfce400e..ed26ea3a7 100644
--- a/src/bun.js/api/server.zig
+++ b/src/bun.js/api/server.zig
@@ -692,6 +692,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
pending_promises_for_abort: u8 = 0,
has_marked_complete: bool = false,
+ has_marked_pending: bool = false,
+
response_jsvalue: JSC.JSValue = JSC.JSValue.zero,
response_protected: bool = false,
response_ptr: ?*JSC.WebCore.Response = null,
@@ -835,7 +837,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return;
}
- if (!ctx.resp.hasResponded()) {
+ if (!ctx.resp.hasResponded() and !ctx.has_marked_pending) {
ctx.renderMissing();
return;
}
@@ -930,6 +932,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
return;
}
+ this.has_marked_pending = true;
this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity };
this.resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this);
this.setAbortHandler();
@@ -948,11 +951,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.response_buf_owned.items.len,
this.shouldCloseConnection(),
)) {
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this);
this.setAbortHandler();
return;
}
-
this.finalize();
}
@@ -1320,6 +1323,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
if (!this.sendfile.has_set_on_writable) {
this.sendfile.has_set_on_writable = true;
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableSendfile, this);
}
@@ -1352,6 +1356,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.finalize();
return true;
} else {
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableBytes, this);
return true;
}
@@ -1365,6 +1370,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
this.response_buf_owned.items.len = 0;
this.finalize();
} else {
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableCompleteResponseBuffer, this);
}
@@ -1885,7 +1891,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
}
// The user returned something that wasn't a promise or a promise with a response
- if (!ctx.resp.hasResponded()) ctx.renderMissing();
+ if (!ctx.resp.hasResponded() and !ctx.has_marked_pending) ctx.renderMissing();
}
pub fn handleResolveStream(req: *RequestContext) void {
@@ -2139,6 +2145,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
} else {
// when it's the last one, we just want to know if it's done
if (stream.isDone()) {
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableResponseBuffer, this);
}
}
@@ -2394,6 +2401,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp
bytes.len,
this.shouldCloseConnection(),
)) {
+ this.has_marked_pending = true;
this.resp.onWritable(*RequestContext, onWritableBytes, this);
// given a blob, we might not have set an abort handler yet
this.setAbortHandler();
diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp
index bacab2cbf..cbf399f20 100644
--- a/src/deps/libuwsockets.cpp
+++ b/src/deps/libuwsockets.cpp
@@ -1025,11 +1025,15 @@ extern "C"
if (ssl)
{
uWS::HttpResponse<true> *uwsRes = (uWS::HttpResponse<true> *)res;
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
uwsRes->end(std::string_view(data, length), close_connection);
}
else
{
uWS::HttpResponse<false> *uwsRes = (uWS::HttpResponse<false> *)res;
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
uwsRes->end(std::string_view(data, length), close_connection);
}
}
@@ -1039,11 +1043,15 @@ extern "C"
if (ssl)
{
uWS::HttpResponse<true> *uwsRes = (uWS::HttpResponse<true> *)res;
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
uwsRes->endWithoutBody(std::nullopt, close_connection);
}
else
{
uWS::HttpResponse<false> *uwsRes = (uWS::HttpResponse<false> *)res;
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
uwsRes->endWithoutBody(std::nullopt, close_connection);
}
}
@@ -1514,12 +1522,22 @@ extern "C"
{
uWS::HttpResponse<true> *uwsRes = (uWS::HttpResponse<true> *)res;
auto pair = uwsRes->tryEnd(std::string_view(bytes, len), total_len, close);
+ if (pair.first) {
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
+ }
+
return pair.first;
}
else
{
uWS::HttpResponse<false> *uwsRes = (uWS::HttpResponse<false> *)res;
auto pair = uwsRes->tryEnd(std::string_view(bytes, len), total_len, close);
+ if (pair.first) {
+ uwsRes->getHttpResponseData()->onWritable = nullptr;
+ uwsRes->onAborted(nullptr);
+ }
+
return pair.first;
}
}
@@ -1551,4 +1569,4 @@ extern "C"
return uwsRes->getNativeHandle();
}
}
-} \ No newline at end of file
+}
diff --git a/test/regression/issue/02499-repro.ts b/test/regression/issue/02499-repro.ts
new file mode 100644
index 000000000..3c50e53e5
--- /dev/null
+++ b/test/regression/issue/02499-repro.ts
@@ -0,0 +1,21 @@
+const server = Bun.serve({
+ port: 0,
+ async fetch(req) {
+ console.log(await req.json());
+ return new Response();
+ },
+});
+console.log(
+ JSON.stringify({
+ hostname: server.hostname,
+ port: server.port,
+ }),
+);
+
+(async function () {
+ for await (let line of console) {
+ if (line === "--CLOSE--") {
+ process.exit(0);
+ }
+ }
+})();
diff --git a/test/regression/issue/02499.test.ts b/test/regression/issue/02499.test.ts
new file mode 100644
index 000000000..da114d95d
--- /dev/null
+++ b/test/regression/issue/02499.test.ts
@@ -0,0 +1,95 @@
+import { expect, it } from "bun:test";
+import { bunExe, bunEnv } from "../../harness.js";
+import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from "fs";
+import { tmpdir } from "os";
+import { dirname, join } from "path";
+import { sleep, spawn, spawnSync, which } from "bun";
+
+// https://github.com/oven-sh/bun/issues/2499
+it("onAborted() and onWritable are not called after receiving an empty response body due to a promise rejection", async testDone => {
+ var timeout = AbortSignal.timeout(10_000);
+ timeout.onabort = e => {
+ testDone(new Error("Test timed out, which means it failed"));
+ };
+
+ const body = new FormData();
+ body.append("hey", "hi");
+
+ // We want to test that the server isn't keeping the connection open in a
+ // zombie-like state when an error occurs due to an unhandled rejected promise
+ //
+ // At the time of writing, this can only happen when:
+ // - development mode is enabled
+ // - the server didn't send the complete response body in one send()
+ // - renderMissing() is called
+ //
+ // In that case, it finalizes the response in the middle of an incomplete body
+ //
+ // On an M1, this reproduces 1 out of every 4 calls to this function
+ // It's inherently going to be flaky without simulating system calls or overriding libc
+ //
+ // So to make sure we catch it
+ // 1) Run this test 40 times
+ // 2) Set a timeout for this test of 10 seconds.
+ //
+ // In debug builds, this test should complete in 1-2 seconds.
+ for (let i = 0; i < 40; i++) {
+ let bunProcess;
+ try {
+ bunProcess = spawn({
+ cmd: [bunExe(), "run", join(import.meta.dir, "./02499-repro.ts")],
+ stdin: "pipe",
+ stderr: "ignore",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ const reader = bunProcess.stdout?.getReader();
+ let hostname, port;
+ {
+ const chunks = [];
+ var decoder = new TextDecoder();
+ while (!hostname && !port) {
+ // @ts-expect-error TODO
+ var { value, done } = await reader?.read();
+ if (done) break;
+ if (chunks.length > 0) {
+ chunks.push(value);
+ }
+ try {
+ if (chunks.length > 0) {
+ value = Buffer.concat(chunks);
+ }
+
+ ({ hostname, port } = JSON.parse(decoder.decode(value).trim()));
+ } catch {
+ chunks.push(value);
+ }
+ }
+ }
+
+ try {
+ await fetch(`http://${hostname}:${port}/upload`, {
+ body,
+ keepalive: false,
+ method: "POST",
+ timeout: true,
+ signal: timeout,
+ });
+ } catch (e) {}
+
+ bunProcess.stdin?.write("--CLOSE--");
+ await bunProcess.stdin?.flush();
+ await bunProcess.stdin?.end();
+ expect(await bunProcess.exited).toBe(0);
+ } catch (e) {
+ timeout.onabort = () => {};
+ testDone(e);
+ throw e;
+ } finally {
+ bunProcess?.kill(9);
+ }
+ }
+ timeout.onabort = () => {};
+ testDone();
+});