Skip to content

Commit 352ec3d

Browse files
authored
fix request signal (#5288)
Fixes #5286. This makes fetch(request) use the signal stored on the Request object. It also keeps fetch level signal overrides working the same way. Tests: - cargo test -p boa_runtime --lib - cargo clippy -p boa_runtime --all-features --all-targets -- -D warnings
1 parent 3f8a996 commit 352ec3d

File tree

4 files changed

+229
-12
lines changed

4 files changed

+229
-12
lines changed

core/runtime/src/fetch/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ async fn fetch_inner<T: Fetcher>(
122122

123123
// The resource parsing is complicated, so we parse it in Rust here (instead of relying on
124124
// `TryFromJs` and friends).
125+
let mut signal = signal;
126+
125127
let request: Request<Vec<u8>> = match resource {
126128
Either::Left(url) => {
127129
let url = url.to_std_string().map_err(JsError::from_rust)?;
@@ -141,10 +143,13 @@ async fn fetch_inner<T: Fetcher>(
141143
return Err(js_error!(TypeError: "Request object is already in use"));
142144
};
143145

146+
signal = signal.or_else(|| request_ref.data().signal());
144147
request_ref.data().inner().clone()
145148
}
146149
};
147150

151+
check_abort(signal.as_ref(), &mut context.borrow_mut())?;
152+
148153
let mut request = if let Some(options) = options {
149154
options.into_request_builder(Some(request))?
150155
} else {

core/runtime/src/fetch/request.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ impl RequestInit {
9696
pub struct JsRequest {
9797
#[unsafe_ignore_trace]
9898
inner: HttpRequest<Vec<u8>>,
99+
signal: Option<JsObject>,
99100
}
100101

101102
impl JsRequest {
@@ -104,11 +105,23 @@ impl JsRequest {
104105
mem::replace(&mut self.inner, HttpRequest::new(Vec::new()))
105106
}
106107

108+
/// Split this request into its HTTP request and abort signal.
109+
fn into_parts(mut self) -> (HttpRequest<Vec<u8>>, Option<JsObject>) {
110+
let request = mem::replace(&mut self.inner, HttpRequest::new(Vec::new()));
111+
let signal = self.signal.take();
112+
(request, signal)
113+
}
114+
107115
/// Get a reference to the inner `http::Request` object.
108116
pub fn inner(&self) -> &HttpRequest<Vec<u8>> {
109117
&self.inner
110118
}
111119

120+
/// Get the abort signal associated with this request, if any.
121+
pub(crate) fn signal(&self) -> Option<JsObject> {
122+
self.signal.clone()
123+
}
124+
112125
/// Get the URI of the request.
113126
pub fn uri(&self) -> &http::Uri {
114127
self.inner.uri()
@@ -123,33 +136,41 @@ impl JsRequest {
123136
input: Either<JsString, JsRequest>,
124137
options: Option<RequestInit>,
125138
) -> JsResult<Self> {
126-
let request = match input {
139+
let (request, signal) = match input {
127140
Either::Left(uri) => {
128141
let uri = http::Uri::try_from(
129142
uri.to_std_string()
130143
.map_err(|_| js_error!(URIError: "URI cannot have unpaired surrogates"))?,
131144
)
132145
.map_err(|_| js_error!(URIError: "Invalid URI"))?;
133-
http::request::Request::builder()
146+
let request = http::request::Request::builder()
134147
.uri(uri)
135148
.body(Vec::<u8>::new())
136-
.map_err(|_| js_error!(Error: "Cannot construct request"))?
149+
.map_err(|_| js_error!(Error: "Cannot construct request"))?;
150+
(request, None)
137151
}
138-
Either::Right(r) => r.into_inner(),
152+
Either::Right(r) => r.into_parts(),
139153
};
140154

141-
if let Some(options) = options {
155+
if let Some(mut options) = options {
156+
let signal = options.take_signal().or(signal);
142157
let inner = options.into_request_builder(Some(request))?;
143-
Ok(Self { inner })
158+
Ok(Self { inner, signal })
144159
} else {
145-
Ok(Self { inner: request })
160+
Ok(Self {
161+
inner: request,
162+
signal,
163+
})
146164
}
147165
}
148166
}
149167

150168
impl From<HttpRequest<Vec<u8>>> for JsRequest {
151169
fn from(inner: HttpRequest<Vec<u8>>) -> Self {
152-
Self { inner }
170+
Self {
171+
inner,
172+
signal: None,
173+
}
153174
}
154175
}
155176

@@ -181,8 +202,6 @@ impl JsRequest {
181202

182203
#[boa(rename = "clone")]
183204
fn clone_request(&self) -> Self {
184-
Self {
185-
inner: self.inner.clone(),
186-
}
205+
self.clone()
187206
}
188207
}

core/runtime/src/fetch/tests/request.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::TestFetcher;
22
use crate::fetch::request::JsRequest;
33
use crate::fetch::response::JsResponse;
44
use crate::test::{TestAction, run_test_actions};
5-
use boa_engine::{js_str, js_string};
5+
use boa_engine::{JsObject, js_str, js_string};
66
use either::Either;
77
use http::{Response, Uri};
88

@@ -215,3 +215,119 @@ fn request_clone_method_is_independent() {
215215
}),
216216
]);
217217
}
218+
219+
#[test]
220+
fn request_stores_signal() {
221+
run_test_actions([
222+
TestAction::inspect_context(|ctx| {
223+
let fetcher = TestFetcher::default();
224+
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
225+
}),
226+
TestAction::run(
227+
r#"
228+
globalThis.ctrl = new AbortController();
229+
globalThis.request = new Request("http://unit.test", {
230+
signal: ctrl.signal,
231+
});
232+
"#,
233+
),
234+
TestAction::inspect_context(|ctx| {
235+
let request = ctx.global_object().get(js_str!("request"), ctx).unwrap();
236+
let request_obj = request.as_object().unwrap();
237+
let request = request_obj.downcast_ref::<JsRequest>().unwrap();
238+
239+
let signal = ctx.global_object().get(js_str!("ctrl"), ctx).unwrap();
240+
let signal = signal
241+
.as_object()
242+
.unwrap()
243+
.get(js_str!("signal"), ctx)
244+
.unwrap()
245+
.as_object()
246+
.unwrap();
247+
248+
let stored_signal = request.signal().expect("request should keep its signal");
249+
assert!(JsObject::equals(&stored_signal, &signal));
250+
}),
251+
]);
252+
}
253+
254+
#[test]
255+
fn request_clone_preserves_signal_without_override() {
256+
run_test_actions([
257+
TestAction::inspect_context(|ctx| {
258+
let fetcher = TestFetcher::default();
259+
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
260+
}),
261+
TestAction::run(
262+
r#"
263+
globalThis.ctrl = new AbortController();
264+
const original = new Request("http://unit.test", {
265+
signal: ctrl.signal,
266+
});
267+
globalThis.cloned = new Request(original, {
268+
headers: { "x-test": "1" },
269+
});
270+
"#,
271+
),
272+
TestAction::inspect_context(|ctx| {
273+
let cloned = ctx.global_object().get(js_str!("cloned"), ctx).unwrap();
274+
let cloned_obj = cloned.as_object().unwrap();
275+
let cloned = cloned_obj.downcast_ref::<JsRequest>().unwrap();
276+
277+
let signal = ctx.global_object().get(js_str!("ctrl"), ctx).unwrap();
278+
let signal = signal
279+
.as_object()
280+
.unwrap()
281+
.get(js_str!("signal"), ctx)
282+
.unwrap()
283+
.as_object()
284+
.unwrap();
285+
286+
let stored_signal = cloned
287+
.signal()
288+
.expect("cloned request should keep its signal");
289+
assert!(JsObject::equals(&stored_signal, &signal));
290+
}),
291+
]);
292+
}
293+
294+
#[test]
295+
fn request_clone_signal_override() {
296+
run_test_actions([
297+
TestAction::inspect_context(|ctx| {
298+
let fetcher = TestFetcher::default();
299+
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
300+
}),
301+
TestAction::run(
302+
r#"
303+
globalThis.ctrl1 = new AbortController();
304+
globalThis.ctrl2 = new AbortController();
305+
const original = new Request("http://unit.test", {
306+
signal: ctrl1.signal,
307+
});
308+
globalThis.cloned = new Request(original, {
309+
signal: ctrl2.signal,
310+
});
311+
"#,
312+
),
313+
TestAction::inspect_context(|ctx| {
314+
let cloned = ctx.global_object().get(js_str!("cloned"), ctx).unwrap();
315+
let cloned_obj = cloned.as_object().unwrap();
316+
let cloned = cloned_obj.downcast_ref::<JsRequest>().unwrap();
317+
318+
let signal = ctx.global_object().get(js_str!("ctrl2"), ctx).unwrap();
319+
let signal = signal
320+
.as_object()
321+
.unwrap()
322+
.get(js_str!("signal"), ctx)
323+
.unwrap()
324+
.as_object()
325+
.unwrap();
326+
327+
let stored_signal = cloned
328+
.signal()
329+
.expect("overridden request should keep the new signal");
330+
assert!(JsObject::equals(&stored_signal, &signal));
331+
}),
332+
]);
333+
}

core/runtime/src/fetch/tests/response.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,83 @@ fn response_getter() {
178178
]);
179179
}
180180

181+
#[test]
182+
fn fetch_request_uses_request_signal() {
183+
run_test_actions([
184+
TestAction::harness(),
185+
TestAction::inspect_context(|ctx| {
186+
register(
187+
&[("http://unit.test", Response::new(b"Hello World".to_vec()))],
188+
ctx,
189+
);
190+
}),
191+
TestAction::run(
192+
r#"
193+
globalThis.response = (async () => {
194+
const ctrl = new AbortController();
195+
const request = new Request("http://unit.test", { signal: ctrl.signal });
196+
ctrl.abort("stop");
197+
198+
try {
199+
await fetch(request);
200+
return "fulfilled";
201+
} catch (e) {
202+
return e;
203+
}
204+
})();
205+
"#,
206+
),
207+
TestAction::inspect_context(|ctx| {
208+
let response = ctx.global_object().get(js_str!("response"), ctx).unwrap();
209+
let response = response.as_promise().unwrap().await_blocking(ctx).unwrap();
210+
assert_eq!(
211+
response.as_string().map(|s| s.to_std_string_escaped()),
212+
Some("stop".into())
213+
);
214+
}),
215+
]);
216+
}
217+
218+
#[test]
219+
fn fetch_options_signal_overrides_request_signal() {
220+
run_test_actions([
221+
TestAction::harness(),
222+
TestAction::inspect_context(|ctx| {
223+
register(
224+
&[("http://unit.test", Response::new(b"Hello World".to_vec()))],
225+
ctx,
226+
);
227+
}),
228+
TestAction::run(
229+
r#"
230+
globalThis.response = (async () => {
231+
const requestCtrl = new AbortController();
232+
const fetchCtrl = new AbortController();
233+
const request = new Request("http://unit.test", {
234+
signal: requestCtrl.signal,
235+
});
236+
fetchCtrl.abort("fetch stop");
237+
238+
try {
239+
await fetch(request, { signal: fetchCtrl.signal });
240+
return "fulfilled";
241+
} catch (e) {
242+
return e;
243+
}
244+
})();
245+
"#,
246+
),
247+
TestAction::inspect_context(|ctx| {
248+
let response = ctx.global_object().get(js_str!("response"), ctx).unwrap();
249+
let response = response.as_promise().unwrap().await_blocking(ctx).unwrap();
250+
assert_eq!(
251+
response.as_string().map(|s| s.to_std_string_escaped()),
252+
Some("fetch stop".into())
253+
);
254+
}),
255+
]);
256+
}
257+
181258
#[test]
182259
fn response_redirect_default_status() {
183260
run_test_actions([

0 commit comments

Comments
 (0)