use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use ipc_channel::ipc;
use js::jsapi::{ExceptionStackBehavior, JS_IsExceptionPending};
use js::jsval::UndefinedValue;
use js::realm::CurrentRealm;
use js::rust::HandleValue;
use js::rust::wrappers::JS_SetPendingException;
use net_traits::request::{
CorsSettings, CredentialsMode, Destination, Referrer, Request as NetTraitsRequest,
RequestBuilder, RequestId, RequestMode, ServiceWorkersMode,
};
use net_traits::{
CoreResourceMsg, CoreResourceThread, FetchChannels, FetchMetadata, FetchResponseMsg,
FilteredMetadata, Metadata, NetworkError, ResourceFetchTiming, cancel_async_fetch,
};
use rustc_hash::FxHashMap;
use script_bindings::cformat;
use serde::{Deserialize, Serialize};
use servo_base::id::WebViewId;
use servo_url::ServoUrl;
use timers::TimerEventRequest;
use uuid::Uuid;
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::codegen::Bindings::WindowBinding::{DeferredRequestInit, WindowMethods};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
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::fetchlaterresult::FetchLaterResult;
use crate::dom::globalscope::GlobalScope;
use crate::dom::headers::Guard;
use crate::dom::performance::performanceresourcetiming::InitiatorType;
use crate::dom::promise::Promise;
use crate::dom::request::Request;
use crate::dom::response::Response;
use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
use crate::dom::window::Window;
use crate::network_listener::{
self, FetchResponseListener, NetworkListener, ResourceTimingListener, submit_timing_data,
};
use crate::realms::{enter_auto_realm, enter_realm};
use crate::script_runtime::CanGc;
#[derive(Default, JSTraceable, MallocSizeOf)]
pub(crate) struct FetchCanceller {
#[no_trace]
request_id: Option<RequestId>,
#[no_trace]
core_resource_thread: Option<CoreResourceThread>,
keep_alive: bool,
}
impl FetchCanceller {
pub(crate) fn new(
request_id: RequestId,
keep_alive: bool,
core_resource_thread: CoreResourceThread,
) -> Self {
Self {
request_id: Some(request_id),
core_resource_thread: Some(core_resource_thread),
keep_alive,
}
}
pub(crate) fn keep_alive(&self) -> bool {
self.keep_alive
}
fn cancel(&mut self) {
if let Some(request_id) = self.request_id.take() {
if let Some(ref core_resource_thread) = self.core_resource_thread {
cancel_async_fetch(vec![request_id], core_resource_thread);
}
}
}
pub(crate) fn ignore(&mut self) {
let _ = self.request_id.take();
}
pub(crate) fn abort(&mut self) {
self.cancel();
}
pub(crate) fn terminate(&mut self) {
self.cancel();
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
pub(crate) struct DeferredFetchRecordId(Uuid);
impl Default for DeferredFetchRecordId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
pub(crate) type QueuedDeferredFetchRecord = Rc<DeferredFetchRecord>;
#[derive(Default, MallocSizeOf)]
pub(crate) struct FetchGroup {
#[conditional_malloc_size_of]
pub(crate) deferred_fetch_records: FxHashMap<DeferredFetchRecordId, QueuedDeferredFetchRecord>,
}
fn request_init_from_request(request: NetTraitsRequest, global: &GlobalScope) -> RequestBuilder {
let mut builder =
RequestBuilder::new(request.target_webview_id, request.url(), request.referrer)
.method(request.method)
.headers(request.headers)
.unsafe_request(request.unsafe_request)
.body(request.body)
.destination(request.destination)
.synchronous(request.synchronous)
.mode(request.mode)
.cache_mode(request.cache_mode)
.use_cors_preflight(request.use_cors_preflight)
.credentials_mode(request.credentials_mode)
.use_url_credentials(request.use_url_credentials)
.referrer_policy(request.referrer_policy)
.pipeline_id(request.pipeline_id)
.redirect_mode(request.redirect_mode)
.integrity_metadata(request.integrity_metadata)
.cryptographic_nonce_metadata(request.cryptographic_nonce_metadata)
.parser_metadata(request.parser_metadata)
.initiator(request.initiator)
.client(global.request_client())
.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);
builder.id = request.id;
builder
}
fn abort_fetch_call(
promise: Rc<Promise>,
request: &Request,
response_object: Option<&Response>,
abort_reason: HandleValue,
global: &GlobalScope,
cx: &mut js::context::JSContext,
) {
promise.reject(cx.into(), abort_reason, CanGc::from_cx(cx));
if let Some(body) = request.body() {
if body.is_readable() {
body.cancel(cx, global, abort_reason);
}
}
let Some(response) = response_object else {
return;
};
if let Some(body) = response.body() {
if body.is_readable() {
body.error(abort_reason, CanGc::from_cx(cx));
}
}
}
#[expect(non_snake_case)]
pub(crate) fn Fetch(
global: &GlobalScope,
input: RequestInfo,
init: RootedTraceableBox<RequestInit>,
cx: &mut CurrentRealm,
) -> Rc<Promise> {
let promise = Promise::new_in_realm(cx);
let response = Response::new(global, CanGc::from_cx(cx));
response
.Headers(CanGc::from_cx(cx))
.set_guard(Guard::Immutable);
let request_object = match Request::Constructor(global, None, CanGc::from_cx(cx), input, init) {
Err(e) => {
response.error_stream(e.clone(), CanGc::from_cx(cx));
promise.reject_error(e, CanGc::from_cx(cx));
return promise;
},
Ok(r) => r,
};
let request = request_object.get_request();
let request_id = request.id;
let signal = request_object.Signal();
if signal.aborted() {
rooted!(&in(cx) let mut abort_reason = UndefinedValue());
signal.Reason(cx.into(), abort_reason.handle_mut());
abort_fetch_call(
promise.clone(),
&request_object,
None,
abort_reason.handle(),
global,
cx,
);
return promise;
}
let keep_alive = request.keep_alive;
let mut request_init = request_init_from_request(request, global);
if global.is::<ServiceWorkerGlobalScope>() {
request_init.service_workers_mode = ServiceWorkersMode::None;
}
let fetch_context = FetchContext {
fetch_promise: Some(TrustedPromise::new(promise.clone())),
response_object: Trusted::new(&*response),
request: Trusted::new(&*request_object),
global: Trusted::new(global),
locally_aborted: false,
canceller: FetchCanceller::new(request_id, keep_alive, global.core_resource_thread()),
url: request_init.url.clone(),
};
let network_listener = NetworkListener::new(
fetch_context,
global.task_manager().networking_task_source().to_sendable(),
);
let fetch_context = network_listener.context.clone();
signal.add(&AbortAlgorithm::Fetch(fetch_context));
global.fetch_with_network_listener(request_init, network_listener);
promise
}
fn queue_deferred_fetch(
request: NetTraitsRequest,
activate_after: Finite<f64>,
global: &GlobalScope,
) -> DeferredFetchRecordId {
let trusted_global = Trusted::new(global);
let mut request = request;
request.client = Some(global.request_client());
request.populate_request_from_client();
request.service_workers_mode = ServiceWorkersMode::None;
request.keep_alive = true;
let deferred_record = Rc::new(DeferredFetchRecord {
request,
invoke_state: Cell::new(DeferredFetchRecordInvokeState::Pending),
activated: Cell::new(false),
});
let deferred_fetch_record_id = global.append_deferred_fetch(deferred_record);
global.schedule_timer(TimerEventRequest {
callback: Box::new(move || {
let global = trusted_global.root();
global.deferred_fetch_record_for_id(&deferred_fetch_record_id).process(&global);
let trusted_global = trusted_global.clone();
global.task_manager().deferred_fetch_task_source().queue(
task!(notify_deferred_record: move || {
trusted_global.root().deferred_fetch_record_for_id(&deferred_fetch_record_id).activate();
}),
);
}),
duration: Duration::from_millis(*activate_after as u64),
});
deferred_fetch_record_id
}
#[expect(non_snake_case, unsafe_code)]
pub(crate) fn FetchLater(
window: &Window,
input: RequestInfo,
init: RootedTraceableBox<DeferredRequestInit>,
can_gc: CanGc,
) -> Fallible<DomRoot<FetchLaterResult>> {
let global_scope = window.upcast();
let document = window.Document();
let request_object = Request::constructor(global_scope, None, can_gc, input, &init.parent)?;
let signal = request_object.Signal();
if signal.aborted() {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut abort_reason = UndefinedValue());
signal.Reason(cx, abort_reason.handle_mut());
unsafe {
assert!(!JS_IsExceptionPending(*cx));
JS_SetPendingException(*cx, abort_reason.handle(), ExceptionStackBehavior::Capture);
}
return Err(Error::JSFailed);
}
let request = request_object.get_request();
let mut activate_after = Finite::wrap(0_f64);
if let Some(init_activate_after) = init.activateAfter.as_ref() {
activate_after = *init_activate_after;
}
if *activate_after < 0.0 {
return Err(Error::Range(c"activateAfter must be at least 0".to_owned()));
}
if !document.is_fully_active() {
return Err(Error::Type(c"Document is not fully active".to_owned()));
}
let url = request.url();
if !matches!(url.scheme(), "http" | "https") {
return Err(Error::Type(c"URL is not http(s)".to_owned()));
}
if !url.is_potentially_trustworthy() {
return Err(Error::Type(c"URL is not trustworthy".to_owned()));
}
if let Some(body) = request.body.as_ref() {
if body.len().is_none_or(|len| len == 0) {
return Err(Error::Type(c"Body is empty".to_owned()));
}
}
let quota = document.available_deferred_fetch_quota(request.url().origin());
let requested = request.total_request_length() as isize;
if quota < requested {
return Err(Error::QuotaExceeded {
quota: Some(Finite::wrap(quota as f64)),
requested: Some(Finite::wrap(requested as f64)),
});
}
let deferred_record_id = queue_deferred_fetch(request, activate_after, global_scope);
signal.add(&AbortAlgorithm::FetchLater(deferred_record_id));
Ok(FetchLaterResult::new(window, deferred_record_id, can_gc))
}
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
pub(crate) enum DeferredFetchRecordInvokeState {
Pending,
Sent,
Aborted,
}
#[derive(MallocSizeOf)]
pub(crate) struct DeferredFetchRecord {
pub(crate) request: NetTraitsRequest,
pub(crate) invoke_state: Cell<DeferredFetchRecordInvokeState>,
activated: Cell<bool>,
}
impl DeferredFetchRecord {
fn activate(&self) {
self.activated.set(true);
}
pub(crate) fn abort(&self) {
self.invoke_state
.set(DeferredFetchRecordInvokeState::Aborted);
}
pub(crate) fn activated_getter_steps(&self) -> bool {
self.activated.get()
}
pub(crate) fn process(&self, global: &GlobalScope) {
if self.invoke_state.get() != DeferredFetchRecordInvokeState::Pending {
return;
}
self.invoke_state.set(DeferredFetchRecordInvokeState::Sent);
let fetch_later_listener = FetchLaterListener {
url: self.request.url(),
global: Trusted::new(global),
};
let request_init = request_init_from_request(self.request.clone(), global);
global.fetch(
request_init,
fetch_later_listener,
global.task_manager().networking_task_source().to_sendable(),
);
}
}
#[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>,
locally_aborted: bool,
canceller: FetchCanceller,
#[no_trace]
url: ServoUrl,
}
impl FetchContext {
pub(crate) fn abort_fetch(
&mut self,
abort_reason: HandleValue,
cx: &mut js::context::JSContext,
) {
self.locally_aborted = true;
self.canceller.abort();
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,
);
}
}
impl FetchResponseListener for FetchContext {
fn process_request_body(&mut self, _: RequestId) {
}
fn process_response(
&mut self,
cx: &mut js::context::JSContext,
_: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>,
) {
if self.locally_aborted {
return;
}
let promise = self
.fetch_promise
.take()
.expect("fetch promise is missing")
.root();
let mut realm = enter_auto_realm(cx, &*promise);
let cx = &mut realm.current_realm();
match fetch_metadata {
Err(error) => {
promise.reject_error(
Error::Type(cformat!("Network error: {:?}", error)),
CanGc::from_cx(cx),
);
self.fetch_promise = Some(TrustedPromise::new(promise));
let response = self.response_object.root();
response.set_type(DOMResponseType::Error, CanGc::from_cx(cx));
response.error_stream(
Error::Type(c"Network error occurred".to_owned()),
CanGc::from_cx(cx),
);
return;
},
Ok(metadata) => match metadata {
FetchMetadata::Unfiltered(m) => {
fill_headers_with_metadata(self.response_object.root(), m, CanGc::from_cx(cx));
self.response_object
.root()
.set_type(DOMResponseType::Default, CanGc::from_cx(cx));
},
FetchMetadata::Filtered { filtered, .. } => match filtered {
FilteredMetadata::Basic(m) => {
fill_headers_with_metadata(
self.response_object.root(),
m,
CanGc::from_cx(cx),
);
self.response_object
.root()
.set_type(DOMResponseType::Basic, CanGc::from_cx(cx));
},
FilteredMetadata::Cors(m) => {
fill_headers_with_metadata(
self.response_object.root(),
m,
CanGc::from_cx(cx),
);
self.response_object
.root()
.set_type(DOMResponseType::Cors, CanGc::from_cx(cx));
},
FilteredMetadata::Opaque => {
self.response_object
.root()
.set_type(DOMResponseType::Opaque, CanGc::from_cx(cx));
},
FilteredMetadata::OpaqueRedirect(url) => {
let r = self.response_object.root();
r.set_type(DOMResponseType::Opaqueredirect, CanGc::from_cx(cx));
r.set_final_url(url);
},
},
},
}
promise.resolve_native(&self.response_object.root(), CanGc::from_cx(cx));
self.fetch_promise = Some(TrustedPromise::new(promise));
}
fn process_response_chunk(
&mut self,
cx: &mut js::context::JSContext,
_: RequestId,
chunk: Vec<u8>,
) {
let response = self.response_object.root();
response.stream_chunk(chunk, CanGc::from_cx(cx));
}
fn process_response_eof(
self,
cx: &mut js::context::JSContext,
_: RequestId,
response: Result<(), NetworkError>,
timing: ResourceFetchTiming,
) {
let response_object = self.response_object.root();
let _ac = enter_realm(&*response_object);
if let Err(ref error) = response {
if *error == NetworkError::DecompressionError {
response_object.error_stream(
Error::Type(c"Network error occurred".to_owned()),
CanGc::from_cx(cx),
);
}
}
response_object.finish(CanGc::from_cx(cx));
network_listener::submit_timing(cx, &self, &response, &timing);
}
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.url.clone())
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.response_object.root().global()
}
}
struct FetchLaterListener {
url: ServoUrl,
global: Trusted<GlobalScope>,
}
impl FetchResponseListener for FetchLaterListener {
fn process_request_body(&mut self, _: RequestId) {}
fn process_response(
&mut self,
_: &mut js::context::JSContext,
_: RequestId,
fetch_metadata: Result<FetchMetadata, NetworkError>,
) {
_ = fetch_metadata;
}
fn process_response_chunk(
&mut self,
_: &mut js::context::JSContext,
_: RequestId,
chunk: Vec<u8>,
) {
_ = chunk;
}
fn process_response_eof(
self,
cx: &mut js::context::JSContext,
_: RequestId,
response: Result<(), NetworkError>,
timing: ResourceFetchTiming,
) {
network_listener::submit_timing(cx, &self, &response, &timing);
}
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 FetchLaterListener {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(InitiatorType::Fetch, self.url.clone())
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.global.root()
}
}
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>);
}
pub(crate) fn load_whole_resource(
request: RequestBuilder,
core_resource_thread: &CoreResourceThread,
global: &GlobalScope,
csp_violations_processor: &dyn CspViolationsProcessor,
cx: &mut js::context::JSContext,
) -> Result<(Metadata, Vec<u8>, bool), 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;
let mut muted_errors = false;
loop {
match action_receiver.recv().unwrap() {
FetchResponseMsg::ProcessRequestBody(..) => {},
FetchResponseMsg::ProcessResponse(_, Ok(m)) => {
muted_errors = m.is_cors_cross_origin();
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(cx, global, url, InitiatorType::Other, timing);
}
return Ok((metadata, buf, muted_errors));
},
FetchResponseMsg::ProcessResponse(_, Err(e)) |
FetchResponseMsg::ProcessResponseEOF(_, Err(e), _) => return Err(e),
FetchResponseMsg::ProcessCspViolations(_, violations) => {
csp_violations_processor.process_csp_violations(violations);
},
}
}
}
pub(crate) trait RequestWithGlobalScope {
fn with_global_scope(self, global: &GlobalScope) -> Self;
}
impl RequestWithGlobalScope for RequestBuilder {
fn with_global_scope(self, global: &GlobalScope) -> Self {
self.insecure_requests_policy(global.insecure_requests_policy())
.has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
.policy_container(global.policy_container())
.client(global.request_client())
.pipeline_id(Some(global.pipeline_id()))
.origin(global.origin().immutable().clone())
.https_state(global.get_https_state())
}
}
#[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,
) -> RequestBuilder {
RequestBuilder::new(webview_id, url, referrer)
.mode(match cors_setting {
Some(_) => RequestMode::CorsMode,
None if same_origin_fallback == Some(true) => RequestMode::SameOrigin,
None => RequestMode::NoCors,
})
.credentials_mode(match cors_setting {
Some(CorsSettings::Anonymous) => CredentialsMode::CredentialsSameOrigin,
_ => CredentialsMode::Include,
})
.destination(destination)
.use_url_credentials(true)
}