Skip to content

Commit c442286

Browse files
committed
feat(wasm): support request timeout
fixes #1135 #1274
1 parent 7047669 commit c442286

File tree

5 files changed

+76
-3
lines changed

5 files changed

+76
-3
lines changed

src/wasm/client.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,10 @@ async fn fetch(req: Request) -> crate::Result<Response> {
216216
}
217217
}
218218

219-
let abort = AbortGuard::new()?;
219+
let mut abort = AbortGuard::new()?;
220+
if let Some(timeout) = req.timeout() {
221+
abort.timeout(*timeout);
222+
}
220223
init.signal(Some(&abort.signal()));
221224

222225
let js_req = web_sys::Request::new_with_str_and_init(req.url().as_str(), &init)

src/wasm/mod.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use wasm_bindgen::JsCast;
1+
use std::convert::TryInto;
2+
use std::time::Duration;
3+
4+
use js_sys::Function;
5+
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
6+
use wasm_bindgen::{JsCast, JsValue};
27
use web_sys::{AbortController, AbortSignal};
38

49
mod body;
@@ -14,6 +19,15 @@ pub use self::client::{Client, ClientBuilder};
1419
pub use self::request::{Request, RequestBuilder};
1520
pub use self::response::Response;
1621

22+
#[wasm_bindgen]
23+
extern "C" {
24+
#[wasm_bindgen(js_name = "setTimeout")]
25+
fn set_timeout(handler: &Function, timeout: i32) -> JsValue;
26+
27+
#[wasm_bindgen(js_name = "clearTimeout")]
28+
fn clear_timeout(handle: JsValue) -> JsValue;
29+
}
30+
1731
async fn promise<T>(promise: js_sys::Promise) -> Result<T, crate::error::BoxError>
1832
where
1933
T: JsCast,
@@ -30,6 +44,7 @@ where
3044
/// A guard that cancels a fetch request when dropped.
3145
struct AbortGuard {
3246
ctrl: AbortController,
47+
timeout: Option<(JsValue, Closure<dyn FnMut()>)>,
3348
}
3449

3550
impl AbortGuard {
@@ -38,16 +53,32 @@ impl AbortGuard {
3853
ctrl: AbortController::new()
3954
.map_err(crate::error::wasm)
4055
.map_err(crate::error::builder)?,
56+
timeout: None,
4157
})
4258
}
4359

4460
fn signal(&self) -> AbortSignal {
4561
self.ctrl.signal()
4662
}
63+
64+
fn timeout(&mut self, timeout: Duration) {
65+
let ctrl = self.ctrl.clone();
66+
let abort = Closure::once(move || ctrl.abort());
67+
let timeout = set_timeout(
68+
abort.as_ref().unchecked_ref::<js_sys::Function>(),
69+
timeout.as_millis().try_into().expect("timeout"),
70+
);
71+
if let Some((id, _)) = self.timeout.replace((timeout, abort)) {
72+
clear_timeout(id);
73+
}
74+
}
4775
}
4876

4977
impl Drop for AbortGuard {
5078
fn drop(&mut self) {
5179
self.ctrl.abort();
80+
if let Some((id, _)) = self.timeout.take() {
81+
clear_timeout(id);
82+
}
5283
}
5384
}

src/wasm/request.rs

+25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::convert::TryFrom;
22
use std::fmt;
3+
use std::time::Duration;
34

45
use bytes::Bytes;
56
use http::{request::Parts, Method, Request as HttpRequest};
@@ -18,6 +19,7 @@ pub struct Request {
1819
url: Url,
1920
headers: HeaderMap,
2021
body: Option<Body>,
22+
timeout: Option<Duration>,
2123
pub(super) cors: bool,
2224
pub(super) credentials: Option<RequestCredentials>,
2325
}
@@ -37,6 +39,7 @@ impl Request {
3739
url,
3840
headers: HeaderMap::new(),
3941
body: None,
42+
timeout: None,
4043
cors: true,
4144
credentials: None,
4245
}
@@ -90,6 +93,18 @@ impl Request {
9093
&mut self.body
9194
}
9295

96+
/// Get the timeout.
97+
#[inline]
98+
pub fn timeout(&self) -> Option<&Duration> {
99+
self.timeout.as_ref()
100+
}
101+
102+
/// Get a mutable reference to the timeout.
103+
#[inline]
104+
pub fn timeout_mut(&mut self) -> &mut Option<Duration> {
105+
&mut self.timeout
106+
}
107+
93108
/// Attempts to clone the `Request`.
94109
///
95110
/// None is returned if a body is which can not be cloned.
@@ -104,6 +119,7 @@ impl Request {
104119
url: self.url.clone(),
105120
headers: self.headers.clone(),
106121
body,
122+
timeout: self.timeout.clone(),
107123
cors: self.cors,
108124
credentials: self.credentials,
109125
})
@@ -233,6 +249,14 @@ impl RequestBuilder {
233249
self
234250
}
235251

252+
/// Enables a request timeout.
253+
pub fn timeout(mut self, timeout: Duration) -> RequestBuilder {
254+
if let Ok(ref mut req) = self.request {
255+
*req.timeout_mut() = Some(timeout);
256+
}
257+
self
258+
}
259+
236260
/// TODO
237261
#[cfg(feature = "multipart")]
238262
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
@@ -449,6 +473,7 @@ where
449473
url,
450474
headers,
451475
body: Some(body.into()),
476+
timeout: None,
452477
cors: true,
453478
credentials: None,
454479
})

tests/timeouts.rs

-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ async fn request_timeout() {
6363
assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str()));
6464
}
6565

66-
#[cfg(not(target_arch = "wasm32"))]
6766
#[tokio::test]
6867
async fn connect_timeout() {
6968
let _ = env_logger::try_init();

tests/wasm_simple.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![cfg(target_arch = "wasm32")]
2+
use std::time::Duration;
23

34
use wasm_bindgen::prelude::*;
45
use wasm_bindgen_test::*;
@@ -22,3 +23,17 @@ async fn simple_example() {
2223
let body = res.text().await.expect("response to utf-8 text");
2324
log(&format!("Body:\n\n{}", body));
2425
}
26+
27+
#[wasm_bindgen_test]
28+
async fn request_with_timeout() {
29+
let client = reqwest::Client::new();
30+
let err = client
31+
.get("https://hyper.rs")
32+
.timeout(Duration::from_millis(10))
33+
.send()
34+
.await
35+
.expect_err("Expected error from aborted request");
36+
37+
assert!(err.is_request());
38+
assert!(format!("{:?}", err).contains("The user aborted a request."));
39+
}

0 commit comments

Comments
 (0)