Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ export const fsStreamInternals = {
},
};

export const nodeHttpInternals = {
kHandle: require("internal/http").kHandle,
getRequestTimeout: $newCppFunction("NodeHTTP.cpp", "jsHTTPGetTimeout", 1) as (handle: unknown) => number,
};

export const arrayBufferViewHasBuffer = $newCppFunction(
"InternalForTesting.cpp",
"jsFunction_arrayBufferViewHasBuffer",
Expand Down
11 changes: 11 additions & 0 deletions src/jsc/bindings/NodeHTTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ extern "C" uWS::HttpRequest* Request__getUWSRequest(void*);
extern "C" void Request__setInternalEventCallback(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" bool NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" int32_t NodeHTTPResponse__getTimeout(void*);
extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" EncodedJSValue Server__setAppFlags(JSC::JSGlobalObject*, EncodedJSValue, bool require_host_header, bool use_strict_method_validation);
extern "C" EncodedJSValue Server__setOnClientError(JSC::JSGlobalObject*, EncodedJSValue, EncodedJSValue);
Expand Down Expand Up @@ -964,6 +965,16 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetTimeout, (JSGlobalObject * globalObject, CallF

return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPGetTimeout, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
JSValue requestValue = callFrame->argument(0);

if (auto* nodeHttpResponse = dynamicDowncast<WebCore::JSNodeHTTPResponse>(requestValue)) {
return JSValue::encode(jsNumber(NodeHTTPResponse__getTimeout(nodeHttpResponse->wrapped())));
}

return JSValue::encode(jsNumber(-1));
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/NodeHTTP.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Bun {
JSC_DECLARE_HOST_FUNCTION(jsHTTPAssignHeaders);
JSC_DECLARE_HOST_FUNCTION(jsHTTPGetHeader);
JSC_DECLARE_HOST_FUNCTION(jsHTTPSetHeader);
JSC_DECLARE_HOST_FUNCTION(jsHTTPGetTimeout);

JSC::Structure* createNodeHTTPServerSocketStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject);
JSC::JSValue createNodeHTTPInternalBinding(Zig::GlobalObject*);
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/server/NodeHTTPResponse.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use core::cell::Cell;
use core::ffi::{c_uint, c_void};
use core::ffi::c_void;
use core::ptr;

use bitflags::bitflags;
Expand Down Expand Up @@ -2134,10 +2134,18 @@ impl NodeHTTPResponse {
return false;
}

// ECMAScript ToUint32 — same bit pattern as
// ToInt32 reinterpreted as unsigned (negative inputs wrap, e.g. -1 → u32::MAX).
let secs = (seconds.to_int32() as c_uint).min(255) as u8;
// `JSValue.toU32` clamps negatives to 0 and saturates at u32::MAX; do not
// use `to_int32() as c_uint` here (that would wrap -1 → u32::MAX → 255).
let secs = seconds.to_u32().min(255) as u8;
raw.timeout(secs);
true
}

#[uws::uws_callback(export = "NodeHTTPResponse__getTimeout")]
pub(crate) fn ffi_get_timeout(&self) -> i32 {
let Some(raw) = self.raw_response.get() else {
return -1;
};
raw.get_timeout() as i32
}
}
13 changes: 13 additions & 0 deletions src/uws_sys/Response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ impl<const SSL: bool> Response<SSL> {
c::uws_res_timeout(Self::ssl_flag(), self.as_raw(), seconds)
}

pub fn get_timeout(&mut self) -> u8 {
c::uws_res_get_timeout(Self::ssl_flag(), self.as_raw())
}

pub fn reset_timeout(&mut self) {
c::uws_res_reset_timeout(Self::ssl_flag(), self.as_raw())
}
Expand Down Expand Up @@ -760,6 +764,14 @@ impl AnyResponse {
any_dispatch!(self, |r| r.timeout(seconds))
}

pub fn get_timeout(self) -> u8 {
match self {
AnyResponse::SSL(ptr) => TLSResponse::as_handle(ptr).get_timeout(),
AnyResponse::TCP(ptr) => TCPResponse::as_handle(ptr).get_timeout(),
AnyResponse::H3(_) => 0,
}
}

pub fn on_data<U: 'static, H>(self, _handler: H, optional_data: *mut U)
where
H: Fn(*mut U, &[u8], bool) + Copy + 'static,
Expand Down Expand Up @@ -1095,6 +1107,7 @@ pub mod c {
close_connection: bool,
);
pub(crate) safe fn uws_res_timeout(ssl: i32, res: &mut uws_res, timeout: u8);
pub(crate) safe fn uws_res_get_timeout(ssl: i32, res: &mut uws_res) -> u8;
pub(crate) safe fn uws_res_reset_timeout(ssl: i32, res: &mut uws_res);
pub(crate) safe fn uws_res_get_buffered_amount(ssl: i32, res: &mut uws_res) -> u64;
pub(crate) fn uws_res_write(
Expand Down
9 changes: 9 additions & 0 deletions src/uws_sys/libuwsockets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,15 @@ extern "C"
uwsRes->setTimeout(seconds);
}
}
uint8_t uws_res_get_timeout(int ssl, uws_res_r res) {
if (ssl) {
uWS::HttpResponse<true> *uwsRes = (uWS::HttpResponse<true> *)res;
return uwsRes->getHttpResponseData()->idleTimeout;
} else {
uWS::HttpResponse<false> *uwsRes = (uWS::HttpResponse<false> *)res;
return uwsRes->getHttpResponseData()->idleTimeout;
}
}

void uws_res_end_without_body(int ssl, uws_res_r res, bool close_connection)
{
Expand Down
66 changes: 66 additions & 0 deletions test/js/node/http/node-http-req-settimeout-clamp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { nodeHttpInternals } from "bun:internal-for-testing";
import { expect, test } from "bun:test";
import { once } from "node:events";
import http from "node:http";
import type { AddressInfo } from "node:net";

const { kHandle, getRequestTimeout } = nodeHttpInternals;

// Server-side IncomingMessage.setTimeout(msecs) flows through
// NodeHTTPResponse__setTimeout with Math.ceil(msecs / 1000). The seconds value
// must be clamped via JSValue.toU32 (negatives → 0, huge → u32::MAX) before
// min(255); a signed-wrap-then-reinterpret (`to_int32() as c_uint`) would turn
// -1 into u32::MAX → 255 instead of 0.
test("server IncomingMessage.setTimeout clamps seconds like JSValue.toU32", async () => {
const results: Record<string, number> = {};
const server = http.createServer((req, res) => {
const handle = (req as any)[kHandle];
for (const [label, msecs] of cases) {
req.setTimeout(msecs);
results[label] = getRequestTimeout(handle);
}
req.setTimeout(0);
res.end("ok");
});
await once(server.listen(0, "127.0.0.1"), "listening");
try {
const { port } = server.address() as AddressInfo;
const body = await new Promise<string>((resolve, reject) => {
const req = http.get({ host: "127.0.0.1", port }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => (data += chunk));
res.on("end", () => resolve(data));
res.on("error", reject);
});
req.on("error", reject);
});
expect(body).toBe("ok");
expect(results).toEqual({
"-1000": 0,
"-1000000": 0,
"-Infinity": 0,
"0": 0,
"2000": 2,
"254000": 254,
"255000": 255,
"256000": 255,
"1e12": 255,
});
} finally {
server.closeAllConnections();
server.close();
}
});

const cases: Array<[string, number]> = [
["-1000", -1000],
["-1000000", -1000000],
["-Infinity", -Infinity],
["0", 0],
["2000", 2000],
["254000", 254000],
["255000", 255000],
["256000", 256000],
["1e12", 1e12],
];
Loading