mirror of
https://github.com/servo/servo.git
synced 2025-09-27 15:20:09 +01:00
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>
547 lines
20 KiB
Rust
547 lines
20 KiB
Rust
/* 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 request’s body is non-null and is readable, then cancel request’s 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 responseObject’s response.
|
||
let Some(response) = response_object else {
|
||
return;
|
||
};
|
||
// Step 5. If response’s body is non-null and is readable, then error response’s 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 requestObject’s request.
|
||
let request = request_object.get_request();
|
||
let timing_type = request.timing_type();
|
||
let request_id = request.id;
|
||
|
||
// Step 4. If requestObject’s signal is aborted, then:
|
||
let signal = request_object.Signal();
|
||
if signal.aborted() {
|
||
// Step 4.1. Abort the fetch() call with p, request, null, and requestObject’s signal’s 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 request’s client’s 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 request’s
|
||
// service-workers mode to "none".
|
||
if global.is::<ServiceWorkerGlobalScope>() {
|
||
request_init.service_workers_mode = ServiceWorkersMode::None;
|
||
}
|
||
|
||
// Step 8. Let relevantRealm be this’s 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 requestObject’s 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 requestObject’s signal’s abort reason.
|
||
self.canceller.cancel();
|
||
|
||
// Step 11.4. Abort the fetch() call with p, request, responseObject,
|
||
// and requestObject’s signal’s 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)
|
||
}
|