Compare commits

...

1 Commits

Author SHA1 Message Date
Pierre Tachoire
ee4f2d400d Add XMLHttpRequest.timeout with curl enforcement
Implement the XHR timeout property end-to-end: the JS-visible
getter/setter stores the value, send() passes it to the HTTP client,
and curl enforces it via CURLOPT_TIMEOUT_MS. On timeout, a `timeout`
event is dispatched instead of `error`, per the XHR spec.
2026-04-01 11:28:26 +02:00
4 changed files with 51 additions and 1 deletions

View File

@@ -928,6 +928,7 @@ pub const Request = struct {
credentials: ?[:0]const u8 = null, credentials: ?[:0]const u8 = null,
notification: *Notification, notification: *Notification,
max_response_size: ?usize = null, max_response_size: ?usize = null,
timeout_ms: u32 = 0,
// This is only relevant for intercepted requests. If a request is flagged // This is only relevant for intercepted requests. If a request is flagged
// as blocking AND is intercepted, then it'll be up to us to wait until // as blocking AND is intercepted, then it'll be up to us to wait until
@@ -1142,6 +1143,11 @@ pub const Transfer = struct {
try conn.setPrivate(self); try conn.setPrivate(self);
// Per-request timeout override (e.g. XHR timeout)
if (req.timeout_ms > 0) {
try conn.setTimeout(req.timeout_ms);
}
// add credentials // add credentials
if (req.credentials) |creds| { if (req.credentials) |creds| {
if (self._auth_challenge != null and self._auth_challenge.?.source == .proxy) { if (self._auth_challenge != null and self._auth_challenge.?.source == .proxy) {

View File

@@ -306,3 +306,27 @@
URL.revokeObjectURL(blobUrl); URL.revokeObjectURL(blobUrl);
}); });
</script> </script>
<script id=xhr_timeout>
// timeout property: default is 0
const req = new XMLHttpRequest();
testing.expectEqual(0, req.timeout);
// timeout can be set and read back
req.timeout = 5000;
testing.expectEqual(5000, req.timeout);
// request with timeout set succeeds normally when server responds in time
testing.async(async (restore) => {
const event = await new Promise((resolve) => {
req.onload = resolve;
req.open('GET', 'http://127.0.0.1:9582/xhr');
req.send();
});
restore();
testing.expectEqual('load', event.type);
testing.expectEqual(200, req.status);
testing.expectEqual(5000, req.timeout);
});
</script>

View File

@@ -63,6 +63,7 @@ _response_type: ResponseType = .text,
_ready_state: ReadyState = .unsent, _ready_state: ReadyState = .unsent,
_on_ready_state_change: ?js.Function.Temp = null, _on_ready_state_change: ?js.Function.Temp = null,
_with_credentials: bool = false, _with_credentials: bool = false,
_timeout: u32 = 0,
const ReadyState = enum(u8) { const ReadyState = enum(u8) {
unsent = 0, unsent = 0,
@@ -180,6 +181,14 @@ pub fn setWithCredentials(self: *XMLHttpRequest, value: bool) !void {
self._with_credentials = value; self._with_credentials = value;
} }
pub fn getTimeout(self: *const XMLHttpRequest) u32 {
return self._timeout;
}
pub fn setTimeout(self: *XMLHttpRequest, value: u32) void {
self._timeout = value;
}
// TODO: this takes an optional 3 more parameters // TODO: this takes an optional 3 more parameters
// TODO: url should be a union, as it can be multiple things // TODO: url should be a union, as it can be multiple things
pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void { pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void {
@@ -253,6 +262,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null, .cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
.cookie_origin = page.url, .cookie_origin = page.url,
.resource_type = .xhr, .resource_type = .xhr,
.timeout_ms = self._timeout,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,
.header_callback = httpHeaderDoneCallback, .header_callback = httpHeaderDoneCallback,
@@ -539,6 +549,7 @@ fn handleError(self: *XMLHttpRequest, err: anyerror) void {
} }
fn _handleError(self: *XMLHttpRequest, err: anyerror) !void { fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
const is_abort = err == error.Abort; const is_abort = err == error.Abort;
const is_timeout = err == error.OperationTimedout;
const new_state: ReadyState = if (is_abort) .unsent else .done; const new_state: ReadyState = if (is_abort) .unsent else .done;
if (new_state != self._ready_state) { if (new_state != self._ready_state) {
@@ -547,8 +558,12 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
try self.stateChanged(new_state, page); try self.stateChanged(new_state, page);
if (is_abort) { if (is_abort) {
try self._proto.dispatch(.abort, null, page); try self._proto.dispatch(.abort, null, page);
} else if (is_timeout) {
try self._proto.dispatch(.timeout, null, page);
} }
if (!is_timeout) {
try self._proto.dispatch(.err, null, page); try self._proto.dispatch(.err, null, page);
}
try self._proto.dispatch(.load_end, null, page); try self._proto.dispatch(.load_end, null, page);
} }
@@ -610,6 +625,7 @@ pub const JsApi = struct {
pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true }); pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true });
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{}); pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
pub const timeout = bridge.accessor(XMLHttpRequest.getTimeout, XMLHttpRequest.setTimeout, .{});
pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true }); pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });
pub const open = bridge.function(XMLHttpRequest.open, .{}); pub const open = bridge.function(XMLHttpRequest.open, .{});
pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true }); pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true });

View File

@@ -234,6 +234,10 @@ pub const Connection = struct {
try libcurl.curl_easy_setopt(self._easy, .url, url.ptr); try libcurl.curl_easy_setopt(self._easy, .url, url.ptr);
} }
pub fn setTimeout(self: *const Connection, timeout_ms: u32) !void {
try libcurl.curl_easy_setopt(self._easy, .timeout_ms, timeout_ms);
}
// a libcurl request has 2 methods. The first is the method that // a libcurl request has 2 methods. The first is the method that
// controls how libcurl behaves. This specifically influences how redirects // controls how libcurl behaves. This specifically influences how redirects
// are handled. For example, if you do a POST and get a 301, libcurl will // are handled. For example, if you do a POST and get a 301, libcurl will