diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index db735474..9f08072e 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -928,6 +928,7 @@ pub const Request = struct { credentials: ?[:0]const u8 = null, notification: *Notification, max_response_size: ?usize = null, + timeout_ms: u32 = 0, // 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 @@ -1142,6 +1143,11 @@ pub const Transfer = struct { 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 if (req.credentials) |creds| { if (self._auth_challenge != null and self._auth_challenge.?.source == .proxy) { diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html index a8683142..f2741fbc 100644 --- a/src/browser/tests/net/xhr.html +++ b/src/browser/tests/net/xhr.html @@ -306,3 +306,27 @@ URL.revokeObjectURL(blobUrl); }); + + diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 5de311ad..2912e54b 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -63,6 +63,7 @@ _response_type: ResponseType = .text, _ready_state: ReadyState = .unsent, _on_ready_state_change: ?js.Function.Temp = null, _with_credentials: bool = false, +_timeout: u32 = 0, const ReadyState = enum(u8) { unsent = 0, @@ -180,6 +181,14 @@ pub fn setWithCredentials(self: *XMLHttpRequest, value: bool) !void { 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: url should be a union, as it can be multiple things 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_origin = page.url, .resource_type = .xhr, + .timeout_ms = self._timeout, .notification = page._session.notification, .start_callback = httpStartCallback, .header_callback = httpHeaderDoneCallback, @@ -539,6 +549,7 @@ fn handleError(self: *XMLHttpRequest, err: anyerror) void { } fn _handleError(self: *XMLHttpRequest, err: anyerror) !void { const is_abort = err == error.Abort; + const is_timeout = err == error.OperationTimedout; const new_state: ReadyState = if (is_abort) .unsent else .done; if (new_state != self._ready_state) { @@ -547,8 +558,12 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void { try self.stateChanged(new_state, page); if (is_abort) { 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); } @@ -610,6 +625,7 @@ pub const JsApi = struct { pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true }); 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 open = bridge.function(XMLHttpRequest.open, .{}); pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true }); diff --git a/src/network/http.zig b/src/network/http.zig index 2bfabac0..48ef0266 100644 --- a/src/network/http.zig +++ b/src/network/http.zig @@ -234,6 +234,10 @@ pub const Connection = struct { 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 // controls how libcurl behaves. This specifically influences how redirects // are handled. For example, if you do a POST and get a 301, libcurl will