Skip to content

Commit 8781d7e

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 f28a753 commit 8781d7e

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
@@ -1078,6 +1078,7 @@ pub const Request = struct {
10781078
resource_type: ResourceType,
10791079
credentials: ?[:0]const u8 = null,
10801080
notification: *Notification,
1081+
timeout_ms: u32 = 0,
10811082

10821083
// This is only relevant for intercepted requests. If a request is flagged
10831084
// as blocking AND is intercepted, then it'll be up to us to wait until
@@ -1365,6 +1366,11 @@ pub const Transfer = struct {
13651366

13661367
conn.transport = .{ .http = self };
13671368

1369+
// Per-request timeout override (e.g. XHR timeout)
1370+
if (req.timeout_ms > 0) {
1371+
try conn.setTimeout(req.timeout_ms);
1372+
}
1373+
13681374
// add credentials
13691375
if (req.credentials) |creds| {
13701376
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 {
@@ -256,6 +265,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
256265
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
257266
.cookie_origin = page.url,
258267
.resource_type = .xhr,
268+
.timeout_ms = self._timeout,
259269
.notification = page._session.notification,
260270
.start_callback = httpStartCallback,
261271
.header_callback = httpHeaderDoneCallback,
@@ -542,6 +552,7 @@ fn handleError(self: *XMLHttpRequest, err: anyerror) void {
542552
}
543553
fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
544554
const is_abort = err == error.Abort;
555+
const is_timeout = err == error.OperationTimedout;
545556

546557
const new_state: ReadyState = if (is_abort) .unsent else .done;
547558
if (new_state != self._ready_state) {
@@ -550,8 +561,12 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
550561
try self.stateChanged(new_state, page);
551562
if (is_abort) {
552563
try self._proto.dispatch(.abort, null, page);
564+
} else if (is_timeout) {
565+
try self._proto.dispatch(.timeout, null, page);
566+
}
567+
if (!is_timeout) {
568+
try self._proto.dispatch(.err, null, page);
553569
}
554-
try self._proto.dispatch(.err, null, page);
555570
try self._proto.dispatch(.load_end, null, page);
556571
}
557572

@@ -613,6 +628,7 @@ pub const JsApi = struct {
613628
pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true });
614629

615630
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
631+
pub const timeout = bridge.accessor(XMLHttpRequest.getTimeout, XMLHttpRequest.setTimeout, .{});
616632
pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });
617633
pub const open = bridge.function(XMLHttpRequest.open, .{});
618634
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
@@ -251,6 +251,10 @@ pub const Connection = struct {
251251
try libcurl.curl_easy_setopt(self._easy, .url, url.ptr);
252252
}
253253

254+
pub fn setTimeout(self: *const Connection, timeout_ms: u32) !void {
255+
try libcurl.curl_easy_setopt(self._easy, .timeout_ms, timeout_ms);
256+
}
257+
254258
// a libcurl request has 2 methods. The first is the method that
255259
// controls how libcurl behaves. This specifically influences how redirects
256260
// are handled. For example, if you do a POST and get a 301, libcurl will

0 commit comments

Comments
 (0)