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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::sync::{Arc, Mutex};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use indexmap::IndexSet; use indexmap::IndexSet;
@ -20,6 +21,7 @@ use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::readablestream::PipeTo; use crate::dom::readablestream::PipeTo;
use crate::fetch::FetchContext;
use crate::realms::InRealm; use crate::realms::InRealm;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; 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> /// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
StreamPiping(PipeTo), StreamPiping(PipeTo),
/// <https://fetch.spec.whatwg.org/#dom-global-fetch> /// <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> /// <https://dom.spec.whatwg.org/#abortsignal>
@ -162,6 +168,14 @@ impl AbortSignal {
reason.set(self.abort_reason.get()); reason.set(self.abort_reason.get());
pipe.abort_with_reason(cx, global, reason.handle(), realm, can_gc); 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. // TODO: match on variant and implement algo steps.
// See the various items of #34866 // See the various items of #34866

View file

@ -21,6 +21,8 @@ use net_traits::{
}; };
use servo_url::ServoUrl; 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::AbortSignalBinding::AbortSignalMethods;
use crate::dom::bindings::codegen::Bindings::RequestBinding::{ use crate::dom::bindings::codegen::Bindings::RequestBinding::{
RequestInfo, RequestInit, RequestMethods, RequestInfo, RequestInit, RequestMethods,
@ -46,12 +48,6 @@ use crate::network_listener::{self, PreInvoke, ResourceTimingListener, submit_ti
use crate::realms::{InRealm, enter_realm}; use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
struct FetchContext {
fetch_promise: Option<TrustedPromise>,
response_object: Trusted<Response>,
resource_timing: ResourceFetchTiming,
}
/// RAII fetch canceller object. /// RAII fetch canceller object.
/// By default initialized to having a /// By default initialized to having a
/// request associated with it, which can be manually cancelled with `cancel`, /// 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> /// <https://fetch.spec.whatwg.org/#abort-fetch>
fn abort_fetch_call( fn abort_fetch_call(
promise: Rc<Promise>, promise: Rc<Promise>,
_request: &NetTraitsRequest, request: &Request,
_response_object: Option<&Response>, response_object: Option<&Response>,
abort_reason: HandleValue, abort_reason: HandleValue,
global: &GlobalScope,
cx: SafeJSContext, cx: SafeJSContext,
can_gc: CanGc, can_gc: CanGc,
) { ) {
// Step 1. Reject promise with error. // Step 1. Reject promise with error.
promise.reject(cx, abort_reason, can_gc); promise.reject(cx, abort_reason, can_gc);
// Step 2. If requests body is non-null and is readable, then cancel requests body with error. // 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. // Step 3. If responseObject is null, then return.
// TODO
// Step 4. Let response be responseObjects response. // 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. // 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> /// <https://fetch.spec.whatwg.org/#dom-global-fetch>
@ -196,6 +202,7 @@ pub(crate) fn Fetch(
// Step 3. Let request be requestObjects request. // Step 3. Let request be requestObjects request.
let request = request_object.get_request(); let request = request_object.get_request();
let timing_type = request.timing_type(); let timing_type = request.timing_type();
let request_id = request.id;
// Step 4. If requestObjects signal is aborted, then: // Step 4. If requestObjects signal is aborted, then:
let signal = request_object.Signal(); let signal = request_object.Signal();
@ -205,9 +212,10 @@ pub(crate) fn Fetch(
signal.Reason(cx, abort_reason.handle_mut()); signal.Reason(cx, abort_reason.handle_mut());
abort_fetch_call( abort_fetch_call(
promise.clone(), promise.clone(),
&request, &request_object,
None, None,
abort_reason.handle(), abort_reason.handle(),
global,
cx, cx,
can_gc, can_gc,
); );
@ -232,43 +240,22 @@ pub(crate) fn Fetch(
// Is `comp` as argument // Is `comp` as argument
// Step 9. Let locallyAborted be false. // Step 9. Let locallyAborted be false.
// TODO
// Step 10. Let controller be null. // 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 { let fetch_context = Arc::new(Mutex::new(FetchContext {
fetch_promise: Some(TrustedPromise::new(promise.clone())), fetch_promise: Some(TrustedPromise::new(promise.clone())),
response_object: Trusted::new(&*response), response_object: Trusted::new(&*response),
request: Trusted::new(&*request_object),
global: Trusted::new(global),
resource_timing: ResourceFetchTiming::new(timing_type), 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. // Step 11. Add the following abort steps to requestObjects signal:
// TODO signal.add(&AbortAlgorithm::Fetch(fetch_context.clone()));
// 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 12.3. If response is a network error, then reject p with a TypeError and abort these steps. // Step 12. Set controller to the result of calling fetch given request and
// Step 12.4. Set responseObject to the result of creating a Response object, given response, "immutable", and relevantRealm. // processResponse given response being these steps:
// Step 12.5. Resolve p with responseObject.
global.fetch( global.fetch(
request_init, request_init,
fetch_context, fetch_context,
@ -279,8 +266,58 @@ pub(crate) fn Fetch(
promise 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 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 { impl FetchResponseListener for FetchContext {
fn process_request_body(&mut self, _: RequestId) { fn process_request_body(&mut self, _: RequestId) {
// TODO // TODO
@ -296,6 +333,10 @@ impl FetchResponseListener for FetchContext {
_: RequestId, _: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>, fetch_metadata: Result<FetchMetadata, NetworkError>,
) { ) {
// Step 12.1. If locallyAborted is true, then abort these steps.
if self.locally_aborted {
return;
}
let promise = self let promise = self
.fetch_promise .fetch_promise
.take() .take()
@ -304,7 +345,8 @@ impl FetchResponseListener for FetchContext {
let _ac = enter_realm(&*promise); let _ac = enter_realm(&*promise);
match fetch_metadata { 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(_) => { Err(_) => {
promise.reject_error( promise.reject_error(
Error::Type("Network error occurred".to_string()), Error::Type("Network error occurred".to_string()),
@ -319,7 +361,8 @@ impl FetchResponseListener for FetchContext {
); );
return; 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 { Ok(metadata) => match metadata {
FetchMetadata::Unfiltered(m) => { FetchMetadata::Unfiltered(m) => {
fill_headers_with_metadata(self.response_object.root(), m, CanGc::note()); 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()); promise.resolve_native(&self.response_object.root(), CanGc::note());
self.fetch_promise = Some(TrustedPromise::new(promise)); self.fetch_promise = Some(TrustedPromise::new(promise));
} }

View file

@ -5,7 +5,6 @@
expected: ERROR expected: ERROR
[general.any.html] [general.any.html]
expected: TIMEOUT
[Request is still 'used' if signal is aborted before fetching] [Request is still 'used' if signal is aborted before fetching]
expected: FAIL expected: FAIL
@ -27,48 +26,11 @@
[Call text() twice on aborted response] [Call text() twice on aborted response]
expected: FAIL expected: FAIL
[Underlying connection is closed when aborting after receiving response]
expected: FAIL
[Underlying connection is closed when aborting after receiving response - no-cors]
expected: FAIL
[Fetch aborted & connection closed when aborted after calling response.arrayBuffer()]
expected: TIMEOUT
[Fetch aborted & connection closed when aborted after calling response.blob()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.formData()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.json()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.text()]
expected: NOTRUN
[Stream errors once aborted. Underlying connection closed.]
expected: NOTRUN
[Stream errors once aborted, after reading. Underlying connection closed.]
expected: NOTRUN
[Stream will not error if body is empty. It's closed with an empty queue before it errors.]
expected: NOTRUN
[Readable stream synchronously cancels with AbortError if aborted before reading]
expected: NOTRUN
[response.bytes() rejects if already aborted] [response.bytes() rejects if already aborted]
expected: FAIL expected: FAIL
[Fetch aborted & connection closed when aborted after calling response.bytes()]
expected: NOTRUN
[general.any.worker.html] [general.any.worker.html]
expected: TIMEOUT
[Request is still 'used' if signal is aborted before fetching] [Request is still 'used' if signal is aborted before fetching]
expected: FAIL expected: FAIL
@ -90,41 +52,5 @@
[Call text() twice on aborted response] [Call text() twice on aborted response]
expected: FAIL expected: FAIL
[Underlying connection is closed when aborting after receiving response]
expected: FAIL
[Underlying connection is closed when aborting after receiving response - no-cors]
expected: FAIL
[Fetch aborted & connection closed when aborted after calling response.arrayBuffer()]
expected: TIMEOUT
[Fetch aborted & connection closed when aborted after calling response.blob()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.formData()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.json()]
expected: NOTRUN
[Fetch aborted & connection closed when aborted after calling response.text()]
expected: NOTRUN
[Stream errors once aborted. Underlying connection closed.]
expected: NOTRUN
[Stream errors once aborted, after reading. Underlying connection closed.]
expected: NOTRUN
[Stream will not error if body is empty. It's closed with an empty queue before it errors.]
expected: NOTRUN
[Readable stream synchronously cancels with AbortError if aborted before reading]
expected: NOTRUN
[response.bytes() rejects if already aborted] [response.bytes() rejects if already aborted]
expected: FAIL expected: FAIL
[Fetch aborted & connection closed when aborted after calling response.bytes()]
expected: NOTRUN

View file

@ -1,7 +0,0 @@
[keepalive.html]
expected: TIMEOUT
[aborting a keepalive fetch should work]
expected: TIMEOUT
[aborting a detached keepalive fetch should not do anything]
expected: NOTRUN