Abort fetch controller when signal is aborted (#39374)

Does not all tests pass because of a mismatch in microtask timing. The
promises are resolved/rejected in the wrong order.

Part of #34866

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe 2025-09-21 14:33:03 +02:00 committed by GitHub
parent 016524bd3c
commit 7abc813fc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 103 additions and 127 deletions

View file

@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::sync::{Arc, Mutex};
use dom_struct::dom_struct;
use indexmap::IndexSet;
@ -20,6 +21,7 @@ use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::readablestream::PipeTo;
use crate::fetch::FetchContext;
use crate::realms::InRealm;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
@ -37,7 +39,11 @@ pub(crate) enum AbortAlgorithm {
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
StreamPiping(PipeTo),
/// <https://fetch.spec.whatwg.org/#dom-global-fetch>
Fetch,
Fetch(
#[no_trace]
#[conditional_malloc_size_of]
Arc<Mutex<FetchContext>>,
),
}
/// <https://dom.spec.whatwg.org/#abortsignal>
@ -162,6 +168,14 @@ impl AbortSignal {
reason.set(self.abort_reason.get());
pipe.abort_with_reason(cx, global, reason.handle(), realm, can_gc);
},
AbortAlgorithm::Fetch(fetch_context) => {
rooted!(in(*cx) let mut reason = UndefinedValue());
reason.set(self.abort_reason.get());
fetch_context
.lock()
.unwrap()
.abort_fetch(reason.handle(), cx, can_gc);
},
_ => {
// TODO: match on variant and implement algo steps.
// See the various items of #34866

View file

@ -21,6 +21,8 @@ use net_traits::{
};
use servo_url::ServoUrl;
use crate::body::BodyMixin;
use crate::dom::abortsignal::AbortAlgorithm;
use crate::dom::bindings::codegen::Bindings::AbortSignalBinding::AbortSignalMethods;
use crate::dom::bindings::codegen::Bindings::RequestBinding::{
RequestInfo, RequestInit, RequestMethods,
@ -46,12 +48,6 @@ use crate::network_listener::{self, PreInvoke, ResourceTimingListener, submit_ti
use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc;
struct FetchContext {
fetch_promise: Option<TrustedPromise>,
response_object: Trusted<Response>,
resource_timing: ResourceFetchTiming,
}
/// RAII fetch canceller object.
/// By default initialized to having a
/// request associated with it, which can be manually cancelled with `cancel`,
@ -146,22 +142,32 @@ fn request_init_from_request(request: NetTraitsRequest) -> RequestBuilder {
/// <https://fetch.spec.whatwg.org/#abort-fetch>
fn abort_fetch_call(
promise: Rc<Promise>,
_request: &NetTraitsRequest,
_response_object: Option<&Response>,
request: &Request,
response_object: Option<&Response>,
abort_reason: HandleValue,
global: &GlobalScope,
cx: SafeJSContext,
can_gc: CanGc,
) {
// Step 1. Reject promise with error.
promise.reject(cx, abort_reason, can_gc);
// Step 2. If requests body is non-null and is readable, then cancel requests body with error.
// TODO
if let Some(body) = request.body() {
if body.is_readable() {
body.cancel(cx, global, abort_reason, can_gc);
}
}
// Step 3. If responseObject is null, then return.
// TODO
// Step 4. Let response be responseObjects response.
// TODO
let Some(response) = response_object else {
return;
};
// Step 5. If responses body is non-null and is readable, then error responses body with error.
// TODO
if let Some(body) = response.body() {
if body.is_readable() {
body.error(abort_reason, can_gc);
}
}
}
/// <https://fetch.spec.whatwg.org/#dom-global-fetch>
@ -196,6 +202,7 @@ pub(crate) fn Fetch(
// Step 3. Let request be requestObjects request.
let request = request_object.get_request();
let timing_type = request.timing_type();
let request_id = request.id;
// Step 4. If requestObjects signal is aborted, then:
let signal = request_object.Signal();
@ -205,9 +212,10 @@ pub(crate) fn Fetch(
signal.Reason(cx, abort_reason.handle_mut());
abort_fetch_call(
promise.clone(),
&request,
&request_object,
None,
abort_reason.handle(),
global,
cx,
can_gc,
);
@ -232,43 +240,22 @@ pub(crate) fn Fetch(
// Is `comp` as argument
// Step 9. Let locallyAborted be false.
// TODO
// Step 10. Let controller be null.
// TODO
// Step 11. Add the following abort steps to requestObjects signal:
// TODO
// Step 11.1. Set locallyAborted to true.
// TODO
// Step 11.2. Assert: controller is non-null.
// TODO
// Step 11.3. Abort controller with requestObjects signals abort reason.
// TODO
// Step 11.4. Abort the fetch() call with p, request, responseObject, and requestObjects signals abort reason.
// TODO
// Step 12. Set controller to the result of calling fetch given request and
// processResponse given response being these steps:
let fetch_context = Arc::new(Mutex::new(FetchContext {
fetch_promise: Some(TrustedPromise::new(promise.clone())),
response_object: Trusted::new(&*response),
request: Trusted::new(&*request_object),
global: Trusted::new(global),
resource_timing: ResourceFetchTiming::new(timing_type),
locally_aborted: false,
canceller: FetchCanceller::new(request_id, global.core_resource_thread()),
}));
// Step 12.1. If locallyAborted is true, then abort these steps.
// TODO
// Step 12.2. If responses aborted flag is set, then:
// TODO
// Step 12.2.1. Let deserializedError be the result of deserialize a serialized
// abort reason given controllers serialized abort reason and relevantRealm.
// TODO
// Step 12.2.2. Abort the fetch() call with p, request, responseObject, and deserializedError.
// TODO
// Step 12.2.3. Abort these steps.
// TODO
// Step 11. Add the following abort steps to requestObjects signal:
signal.add(&AbortAlgorithm::Fetch(fetch_context.clone()));
// Step 12.3. If response is a network error, then reject p with a TypeError and abort these steps.
// Step 12.4. Set responseObject to the result of creating a Response object, given response, "immutable", and relevantRealm.
// Step 12.5. Resolve p with responseObject.
// Step 12. Set controller to the result of calling fetch given request and
// processResponse given response being these steps:
global.fetch(
request_init,
fetch_context,
@ -279,8 +266,58 @@ pub(crate) fn Fetch(
promise
}
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct FetchContext {
#[ignore_malloc_size_of = "unclear ownership semantics"]
fetch_promise: Option<TrustedPromise>,
response_object: Trusted<Response>,
request: Trusted<Request>,
global: Trusted<GlobalScope>,
#[no_trace]
resource_timing: ResourceFetchTiming,
locally_aborted: bool,
canceller: FetchCanceller,
}
impl PreInvoke for FetchContext {}
impl FetchContext {
/// Step 11 of <https://fetch.spec.whatwg.org/#dom-global-fetch>
pub(crate) fn abort_fetch(
&mut self,
abort_reason: HandleValue,
cx: SafeJSContext,
can_gc: CanGc,
) {
// Step 11.1. Set locallyAborted to true.
self.locally_aborted = true;
// Step 11.2. Assert: controller is non-null.
//
// N/a, that's self
// Step 11.3. Abort controller with requestObjects signals abort reason.
self.canceller.cancel();
// Step 11.4. Abort the fetch() call with p, request, responseObject,
// and requestObjects signals abort reason.
let promise = self
.fetch_promise
.take()
.expect("fetch promise is missing")
.root();
abort_fetch_call(
promise,
&self.request.root(),
Some(&self.response_object.root()),
abort_reason,
&self.global.root(),
cx,
can_gc,
);
}
}
/// Step 12 of <https://fetch.spec.whatwg.org/#dom-global-fetch>
impl FetchResponseListener for FetchContext {
fn process_request_body(&mut self, _: RequestId) {
// TODO
@ -296,6 +333,10 @@ impl FetchResponseListener for FetchContext {
_: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>,
) {
// Step 12.1. If locallyAborted is true, then abort these steps.
if self.locally_aborted {
return;
}
let promise = self
.fetch_promise
.take()
@ -304,7 +345,8 @@ impl FetchResponseListener for FetchContext {
let _ac = enter_realm(&*promise);
match fetch_metadata {
// Step 4.1
// Step 12.3. If response is a network error, then reject
// p with a TypeError and abort these steps.
Err(_) => {
promise.reject_error(
Error::Type("Network error occurred".to_string()),
@ -319,7 +361,8 @@ impl FetchResponseListener for FetchContext {
);
return;
},
// Step 4.2
// Step 12.4. Set responseObject to the result of creating a Response object,
// given response, "immutable", and relevantRealm.
Ok(metadata) => match metadata {
FetchMetadata::Unfiltered(m) => {
fill_headers_with_metadata(self.response_object.root(), m, CanGc::note());
@ -354,7 +397,7 @@ impl FetchResponseListener for FetchContext {
},
}
// Step 4.3
// Step 12.5. Resolve p with responseObject.
promise.resolve_native(&self.response_object.root(), CanGc::note());
self.fetch_promise = Some(TrustedPromise::new(promise));
}