Skip to content

Commit ee4f2d4

Browse files
committed
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.
1 parent ca8361f commit ee4f2d4

4 files changed

Lines changed: 51 additions & 1 deletion

File tree

src/browser/HttpClient.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@ pub const Request = struct {
928928
credentials: ?[:0]const u8 = null,
929929
notification: *Notification,
930930
max_response_size: ?usize = null,
931+
timeout_ms: u32 = 0,
931932

932933
// This is only relevant for intercepted requests. If a request is flagged
933934
// as blocking AND is intercepted, then it'll be up to us to wait until
@@ -1142,6 +1143,11 @@ pub const Transfer = struct {
11421143

11431144
try conn.setPrivate(self);
11441145

1146+
// Per-request timeout override (e.g. XHR timeout)
1147+
if (req.timeout_ms > 0) {
1148+
try conn.setTimeout(req.timeout_ms);
1149+
}
1150+
11451151
// add credentials
11461152
if (req.credentials) |creds| {
11471153
if (self._auth_challenge != null and self._auth_challenge.?.source == .proxy) {

src/browser/tests/net/xhr.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,27 @@
306306
URL.revokeObjectURL(blobUrl);
307307
});
308308
</script>
309+
310+
<script id=xhr_timeout>
311+
// timeout property: default is 0
312+
const req = new XMLHttpRequest();
313+
testing.expectEqual(0, req.timeout);
314+
315+
// timeout can be set and read back
316+
req.timeout = 5000;
317+
testing.expectEqual(5000, req.timeout);
318+
319+
// request with timeout set succeeds normally when server responds in time
320+
testing.async(async (restore) => {
321+
const event = await new Promise((resolve) => {
322+
req.onload = resolve;
323+
req.open('GET', 'http://127.0.0.1:9582/xhr');
324+
req.send();
325+
});
326+
327+
restore();
328+
testing.expectEqual('load', event.type);
329+
testing.expectEqual(200, req.status);
330+
testing.expectEqual(5000, req.timeout);
331+
});
332+
</script>

src/browser/webapi/net/XMLHttpRequest.zig

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ _response_type: ResponseType = .text,
6363
_ready_state: ReadyState = .unsent,
6464
_on_ready_state_change: ?js.Function.Temp = null,
6565
_with_credentials: bool = false,
66+
_timeout: u32 = 0,
6667

6768
const ReadyState = enum(u8) {
6869
unsent = 0,
@@ -180,6 +181,14 @@ pub fn setWithCredentials(self: *XMLHttpRequest, value: bool) !void {
180181
self._with_credentials = value;
181182
}
182183

184+
pub fn getTimeout(self: *const XMLHttpRequest) u32 {
185+
return self._timeout;
186+
}
187+
188+
pub fn setTimeout(self: *XMLHttpRequest, value: u32) void {
189+
self._timeout = value;
190+
}
191+
183192
// TODO: this takes an optional 3 more parameters
184193
// TODO: url should be a union, as it can be multiple things
185194
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 {
253262
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
254263
.cookie_origin = page.url,
255264
.resource_type = .xhr,
265+
.timeout_ms = self._timeout,
256266
.notification = page._session.notification,
257267
.start_callback = httpStartCallback,
258268
.header_callback = httpHeaderDoneCallback,
@@ -539,6 +549,7 @@ fn handleError(self: *XMLHttpRequest, err: anyerror) void {
539549
}
540550
fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
541551
const is_abort = err == error.Abort;
552+
const is_timeout = err == error.OperationTimedout;
542553

543554
const new_state: ReadyState = if (is_abort) .unsent else .done;
544555
if (new_state != self._ready_state) {
@@ -547,8 +558,12 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
547558
try self.stateChanged(new_state, page);
548559
if (is_abort) {
549560
try self._proto.dispatch(.abort, null, page);
561+
} else if (is_timeout) {
562+
try self._proto.dispatch(.timeout, null, page);
563+
}
564+
if (!is_timeout) {
565+
try self._proto.dispatch(.err, null, page);
550566
}
551-
try self._proto.dispatch(.err, null, page);
552567
try self._proto.dispatch(.load_end, null, page);
553568
}
554569

@@ -610,6 +625,7 @@ pub const JsApi = struct {
610625
pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true });
611626

612627
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
628+
pub const timeout = bridge.accessor(XMLHttpRequest.getTimeout, XMLHttpRequest.setTimeout, .{});
613629
pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });
614630
pub const open = bridge.function(XMLHttpRequest.open, .{});
615631
pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true });

src/network/http.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ pub const Connection = struct {
234234
try libcurl.curl_easy_setopt(self._easy, .url, url.ptr);
235235
}
236236

237+
pub fn setTimeout(self: *const Connection, timeout_ms: u32) !void {
238+
try libcurl.curl_easy_setopt(self._easy, .timeout_ms, timeout_ms);
239+
}
240+
237241
// a libcurl request has 2 methods. The first is the method that
238242
// controls how libcurl behaves. This specifically influences how redirects
239243
// are handled. For example, if you do a POST and get a 301, libcurl will

0 commit comments

Comments
 (0)