servo/components/script/fetch.rs
Tim van der Lippe 7abc813fc3
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>
2025-09-21 12:33:03 +00:00

547 lines
20 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use base::id::WebViewId;
use ipc_channel::ipc;
use js::jsval::UndefinedValue;
use js::rust::HandleValue;
use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
use net_traits::request::{
CorsSettings, CredentialsMode, Destination, InsecureRequestsPolicy, Referrer,
Request as NetTraitsRequest, RequestBuilder, RequestId, RequestMode, ServiceWorkersMode,
};
use net_traits::{
CoreResourceMsg, CoreResourceThread, FetchChannels, FetchMetadata, FetchResponseListener,
FetchResponseMsg, FilteredMetadata, Metadata, NetworkError, ResourceFetchTiming,
ResourceTimingType, cancel_async_fetch,
};
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,
};
use crate::dom::bindings::codegen::Bindings::ResponseBinding::Response_Binding::ResponseMethods;
use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::import::module::SafeJSContext;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::trace::RootedTraceableBox;
use crate::dom::csp::{GlobalCspReporting, Violation};
use crate::dom::globalscope::GlobalScope;
use crate::dom::headers::Guard;
use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::promise::Promise;
use crate::dom::request::Request;
use crate::dom::response::Response;
use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
use crate::network_listener::{self, PreInvoke, ResourceTimingListener, submit_timing_data};
use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc;
/// RAII fetch canceller object.
/// By default initialized to having a
/// request associated with it, which can be manually cancelled with `cancel`,
/// or automatically cancelled on drop.
/// Calling `ignore` will sever the relationship with the request,
/// meaning it cannot be cancelled through this canceller from that point on.
#[derive(Default, JSTraceable, MallocSizeOf)]
pub(crate) struct FetchCanceller {
#[no_trace]
request_id: Option<RequestId>,
#[no_trace]
core_resource_thread: Option<CoreResourceThread>,
}
impl FetchCanceller {
/// Create a FetchCanceller associated with a request,
// and a particular(public vs private) resource thread.
pub(crate) fn new(request_id: RequestId, core_resource_thread: CoreResourceThread) -> Self {
Self {
request_id: Some(request_id),
core_resource_thread: Some(core_resource_thread),
}
}
/// Cancel a fetch if it is ongoing
pub(crate) fn cancel(&mut self) {
if let Some(request_id) = self.request_id.take() {
// stop trying to make fetch happen
// it's not going to happen
if let Some(ref core_resource_thread) = self.core_resource_thread {
// No error handling here. Cancellation is a courtesy call,
// we don't actually care if the other side heard.
cancel_async_fetch(vec![request_id], core_resource_thread);
}
}
}
/// Use this if you don't want it to send a cancellation request
/// on drop (e.g. if the fetch completes)
pub(crate) fn ignore(&mut self) {
let _ = self.request_id.take();
}
}
impl Drop for FetchCanceller {
fn drop(&mut self) {
self.cancel()
}
}
fn request_init_from_request(request: NetTraitsRequest) -> RequestBuilder {
RequestBuilder {
id: request.id,
method: request.method.clone(),
url: request.url(),
headers: request.headers.clone(),
unsafe_request: request.unsafe_request,
body: request.body.clone(),
service_workers_mode: ServiceWorkersMode::All,
destination: request.destination,
synchronous: request.synchronous,
mode: request.mode.clone(),
cache_mode: request.cache_mode,
use_cors_preflight: request.use_cors_preflight,
credentials_mode: request.credentials_mode,
use_url_credentials: request.use_url_credentials,
origin: GlobalScope::current()
.expect("No current global object")
.origin()
.immutable()
.clone(),
referrer: request.referrer.clone(),
referrer_policy: request.referrer_policy,
pipeline_id: request.pipeline_id,
target_webview_id: request.target_webview_id,
redirect_mode: request.redirect_mode,
integrity_metadata: request.integrity_metadata.clone(),
cryptographic_nonce_metadata: request.cryptographic_nonce_metadata.clone(),
url_list: vec![],
parser_metadata: request.parser_metadata,
initiator: request.initiator,
policy_container: request.policy_container,
insecure_requests_policy: request.insecure_requests_policy,
has_trustworthy_ancestor_origin: request.has_trustworthy_ancestor_origin,
https_state: request.https_state,
response_tainting: request.response_tainting,
crash: None,
}
}
/// <https://fetch.spec.whatwg.org/#abort-fetch>
fn abort_fetch_call(
promise: Rc<Promise>,
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.
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 4. Let response be responseObjects response.
let Some(response) = response_object else {
return;
};
// Step 5. If responses body is non-null and is readable, then error responses body with error.
if let Some(body) = response.body() {
if body.is_readable() {
body.error(abort_reason, can_gc);
}
}
}
/// <https://fetch.spec.whatwg.org/#dom-global-fetch>
#[allow(non_snake_case)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn Fetch(
global: &GlobalScope,
input: RequestInfo,
init: RootedTraceableBox<RequestInit>,
comp: InRealm,
can_gc: CanGc,
) -> Rc<Promise> {
// Step 1. Let p be a new promise.
let promise = Promise::new_in_current_realm(comp, can_gc);
let cx = GlobalScope::get_cx();
// Step 7. Let responseObject be null.
// NOTE: We do initialize the object earlier earlier so we can use it to track errors
let response = Response::new(global, can_gc);
response.Headers(can_gc).set_guard(Guard::Immutable);
// Step 2. Let requestObject be the result of invoking the initial value of Request as constructor
// with input and init as arguments. If this throws an exception, reject p with it and return p.
let request_object = match Request::Constructor(global, None, can_gc, input, init) {
Err(e) => {
response.error_stream(e.clone(), can_gc);
promise.reject_error(e, can_gc);
return promise;
},
Ok(r) => r,
};
// 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();
if signal.aborted() {
// Step 4.1. Abort the fetch() call with p, request, null, and requestObjects signals abort reason.
rooted!(in(*cx) let mut abort_reason = UndefinedValue());
signal.Reason(cx, abort_reason.handle_mut());
abort_fetch_call(
promise.clone(),
&request_object,
None,
abort_reason.handle(),
global,
cx,
can_gc,
);
// Step 4.2. Return p.
return promise;
}
// Step 5. Let globalObject be requests clients global object.
// NOTE: We already get the global object as an argument
let mut request_init = request_init_from_request(request);
request_init.policy_container =
RequestPolicyContainer::PolicyContainer(global.policy_container());
// Step 6. If globalObject is a ServiceWorkerGlobalScope object, then set requests
// service-workers mode to "none".
if global.is::<ServiceWorkerGlobalScope>() {
request_init.service_workers_mode = ServiceWorkersMode::None;
}
// Step 8. Let relevantRealm be thiss relevant realm.
//
// Is `comp` as argument
// Step 9. Let locallyAborted be false.
// Step 10. Let controller be null.
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 11. Add the following abort steps to requestObjects signal:
signal.add(&AbortAlgorithm::Fetch(fetch_context.clone()));
// 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,
global.task_manager().networking_task_source().to_sendable(),
);
// Step 13. Return p.
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
}
fn process_request_eof(&mut self, _: RequestId) {
// TODO
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn process_response(
&mut self,
_: 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()
.expect("fetch promise is missing")
.root();
let _ac = enter_realm(&*promise);
match fetch_metadata {
// 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()),
CanGc::note(),
);
self.fetch_promise = Some(TrustedPromise::new(promise));
let response = self.response_object.root();
response.set_type(DOMResponseType::Error, CanGc::note());
response.error_stream(
Error::Type("Network error occurred".to_string()),
CanGc::note(),
);
return;
},
// 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());
self.response_object
.root()
.set_type(DOMResponseType::Default, CanGc::note());
},
FetchMetadata::Filtered { filtered, .. } => match filtered {
FilteredMetadata::Basic(m) => {
fill_headers_with_metadata(self.response_object.root(), m, CanGc::note());
self.response_object
.root()
.set_type(DOMResponseType::Basic, CanGc::note());
},
FilteredMetadata::Cors(m) => {
fill_headers_with_metadata(self.response_object.root(), m, CanGc::note());
self.response_object
.root()
.set_type(DOMResponseType::Cors, CanGc::note());
},
FilteredMetadata::Opaque => {
self.response_object
.root()
.set_type(DOMResponseType::Opaque, CanGc::note());
},
FilteredMetadata::OpaqueRedirect(url) => {
let r = self.response_object.root();
r.set_type(DOMResponseType::Opaqueredirect, CanGc::note());
r.set_final_url(url);
},
},
},
}
// Step 12.5. Resolve p with responseObject.
promise.resolve_native(&self.response_object.root(), CanGc::note());
self.fetch_promise = Some(TrustedPromise::new(promise));
}
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
let response = self.response_object.root();
response.stream_chunk(chunk, CanGc::note());
}
fn process_response_eof(
&mut self,
_: RequestId,
_response: Result<ResourceFetchTiming, NetworkError>,
) {
let response = self.response_object.root();
let _ac = enter_realm(&*response);
response.finish(CanGc::note());
// TODO
// ... trailerObject is not supported in Servo yet.
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
// navigation submission is handled in servoparser/mod.rs
if self.resource_timing.timing_type == ResourceTimingType::Resource {
network_listener::submit_timing(self, CanGc::note())
}
}
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
let global = &self.resource_timing_global();
global.report_csp_violations(violations, None, None);
}
}
impl ResourceTimingListener for FetchContext {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(
InitiatorType::Fetch,
self.resource_timing_global().get_url().clone(),
)
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.response_object.root().global()
}
}
fn fill_headers_with_metadata(r: DomRoot<Response>, m: Metadata, can_gc: CanGc) {
r.set_headers(m.headers, can_gc);
r.set_status(&m.status);
r.set_final_url(m.final_url);
r.set_redirected(m.redirected);
}
pub(crate) trait CspViolationsProcessor {
fn process_csp_violations(&self, violations: Vec<Violation>);
}
/// Convenience function for synchronously loading a whole resource.
pub(crate) fn load_whole_resource(
request: RequestBuilder,
core_resource_thread: &CoreResourceThread,
global: &GlobalScope,
csp_violations_processor: &dyn CspViolationsProcessor,
can_gc: CanGc,
) -> Result<(Metadata, Vec<u8>), NetworkError> {
let request = request.https_state(global.get_https_state());
let (action_sender, action_receiver) = ipc::channel().unwrap();
let url = request.url.clone();
core_resource_thread
.send(CoreResourceMsg::Fetch(
request,
FetchChannels::ResponseMsg(action_sender),
))
.unwrap();
let mut buf = vec![];
let mut metadata = None;
loop {
match action_receiver.recv().unwrap() {
FetchResponseMsg::ProcessRequestBody(..) | FetchResponseMsg::ProcessRequestEOF(..) => {
},
FetchResponseMsg::ProcessResponse(_, Ok(m)) => {
metadata = Some(match m {
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
})
},
FetchResponseMsg::ProcessResponseChunk(_, data) => buf.extend_from_slice(&data),
FetchResponseMsg::ProcessResponseEOF(_, Ok(_)) => {
let metadata = metadata.unwrap();
if let Some(timing) = &metadata.timing {
submit_timing_data(global, url, InitiatorType::Other, timing, can_gc);
}
return Ok((metadata, buf));
},
FetchResponseMsg::ProcessResponse(_, Err(e)) |
FetchResponseMsg::ProcessResponseEOF(_, Err(e)) => return Err(e),
FetchResponseMsg::ProcessCspViolations(_, violations) => {
csp_violations_processor.process_csp_violations(violations);
},
}
}
}
/// <https://html.spec.whatwg.org/multipage/#create-a-potential-cors-request>
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_a_potential_cors_request(
webview_id: Option<WebViewId>,
url: ServoUrl,
destination: Destination,
cors_setting: Option<CorsSettings>,
same_origin_fallback: Option<bool>,
referrer: Referrer,
insecure_requests_policy: InsecureRequestsPolicy,
has_trustworthy_ancestor_origin: bool,
policy_container: PolicyContainer,
) -> RequestBuilder {
RequestBuilder::new(webview_id, url, referrer)
// https://html.spec.whatwg.org/multipage/#create-a-potential-cors-request
// Step 1
.mode(match cors_setting {
Some(_) => RequestMode::CorsMode,
None if same_origin_fallback == Some(true) => RequestMode::SameOrigin,
None => RequestMode::NoCors,
})
// https://html.spec.whatwg.org/multipage/#create-a-potential-cors-request
// Step 3-4
.credentials_mode(match cors_setting {
Some(CorsSettings::Anonymous) => CredentialsMode::CredentialsSameOrigin,
_ => CredentialsMode::Include,
})
// Step 5
.destination(destination)
.use_url_credentials(true)
.insecure_requests_policy(insecure_requests_policy)
.has_trustworthy_ancestor_origin(has_trustworthy_ancestor_origin)
.policy_container(policy_container)
}