script: Move navigation fetching to the ScriptThread (#34919)

This allows reusing the asynchrnous fetch mechanism that we use for page
resources and is likely a step toward removing the `FetchThread`.

Benefits:
 - Reduces IPC traffic during navigation. Now instead of bouncing
   between the constellation and the `ScriptThread` responses are sent
   directly to the `ScriptThread`.
 - Allows cancelling loads after redirects, which was not possible
   before.

There is the question of what to do when a redirect is cross-origin
(#23037). This currently isn't handled properly as the `Constellation`
sends data to the same `Pipeline` that initiated the load. This change
doesn't fix this issue, but does make it more possible for the
`ScriptThread` to shut down the pipeline and ask the `Constellation` to
replace it with a new one.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-10 13:19:40 +01:00 committed by GitHub
parent 73c0701c83
commit fbd77b4524
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 420 additions and 460 deletions

View file

@ -129,9 +129,9 @@ use keyboard_types::{CompositionEvent, KeyboardEvent};
use log::{debug, error, info, trace, warn};
use media::{GLPlayerThreads, WindowGLContext};
use net_traits::pub_domains::reg_host;
use net_traits::request::{Referrer, RequestBuilder};
use net_traits::request::Referrer;
use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use net_traits::{self, FetchResponseMsg, IpcSend, ReferrerPolicy, ResourceThreads};
use net_traits::{self, IpcSend, ReferrerPolicy, ResourceThreads};
use profile_traits::{mem, time};
use script_layout_interface::{LayoutFactory, ScriptThreadFactory};
use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent};
@ -166,7 +166,6 @@ use crate::browsingcontext::{
NewBrowsingContextInfo,
};
use crate::event_loop::EventLoop;
use crate::network_listener::NetworkListener;
use crate::pipeline::{InitialPipelineState, Pipeline};
use crate::serviceworker::ServiceWorkerUnprivilegedContent;
use crate::session_history::{
@ -317,12 +316,6 @@ pub struct Constellation<STF, SWF> {
/// This is the constellation's view of `layout_sender`.
layout_receiver: Receiver<Result<FromLayoutMsg, IpcError>>,
/// A channel for network listener to send messages to the constellation.
network_listener_sender: Sender<(PipelineId, FetchResponseMsg)>,
/// A channel for the constellation to receive messages from network listener.
network_listener_receiver: Receiver<(PipelineId, FetchResponseMsg)>,
/// A channel for the constellation to receive messages from the compositor thread.
compositor_receiver: Receiver<FromCompositorMsg>,
@ -685,8 +678,6 @@ where
layout_ipc_receiver,
);
let (network_listener_sender, network_listener_receiver) = unbounded();
let swmanager_receiver =
route_ipc_receiver_to_new_crossbeam_receiver_preserving_errors(
swmanager_ipc_receiver,
@ -715,8 +706,6 @@ where
compositor_receiver,
layout_factory,
layout_receiver,
network_listener_sender,
network_listener_receiver,
embedder_proxy: state.embedder_proxy,
compositor_proxy: state.compositor_proxy,
webviews: WebViewManager::default(),
@ -1156,15 +1145,12 @@ where
)]
fn handle_request(&mut self) {
#[derive(Debug)]
// FIXME: https://github.com/servo/servo/issues/34591
#[expect(clippy::large_enum_variant)]
enum Request {
PipelineNamespace(PipelineNamespaceRequest),
Script((PipelineId, FromScriptMsg)),
BackgroundHangMonitor(HangMonitorAlert),
Compositor(FromCompositorMsg),
Layout(FromLayoutMsg),
NetworkListener((PipelineId, FetchResponseMsg)),
FromSWManager(SWManagerMsg),
}
// Get one incoming request.
@ -1198,11 +1184,6 @@ where
recv(self.layout_receiver) -> msg => {
msg.expect("Unexpected layout channel panic in constellation").map(Request::Layout)
}
recv(self.network_listener_receiver) -> msg => {
Ok(Request::NetworkListener(
msg.expect("Unexpected network listener channel panic in constellation")
))
}
recv(self.swmanager_receiver) -> msg => {
msg.expect("Unexpected SW channel panic in constellation").map(Request::FromSWManager)
}
@ -1228,9 +1209,6 @@ where
Request::Layout(message) => {
self.handle_request_from_layout(message);
},
Request::NetworkListener(message) => {
self.handle_request_from_network_listener(message);
},
Request::FromSWManager(message) => {
self.handle_request_from_swmanager(message);
},
@ -1263,26 +1241,6 @@ where
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_request_from_network_listener(&mut self, message: (PipelineId, FetchResponseMsg)) {
let (id, message_) = message;
let result = match self.pipelines.get(&id) {
Some(pipeline) => {
let msg = ConstellationControlMsg::NavigationResponse(id, message_);
pipeline.event_loop.send(msg)
},
None => {
return warn!("{}: Got fetch data after closure!", id);
},
};
if let Err(e) = result {
self.handle_send_error(id, e);
}
}
fn handle_request_from_swmanager(&mut self, message: SWManagerMsg) {
match message {
SWManagerMsg::PostMessageToClient => {
@ -1615,10 +1573,6 @@ where
FromScriptMsg::DiscardTopLevelBrowsingContext => {
self.handle_close_top_level_browsing_context(source_top_ctx_id);
},
FromScriptMsg::InitiateNavigateRequest(req_init, cancel_chan) => {
self.handle_navigate_request(source_pipeline_id, req_init, cancel_chan);
},
FromScriptMsg::ScriptLoadedURLInIFrame(load_info) => {
self.handle_script_loaded_url_in_iframe_msg(load_info);
},
@ -3202,26 +3156,6 @@ where
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_navigate_request(
&self,
id: PipelineId,
request_builder: RequestBuilder,
cancel_chan: IpcReceiver<()>,
) {
let listener = NetworkListener::new(
request_builder,
id,
self.public_resource_threads.clone(),
self.network_listener_sender.clone(),
);
listener.initiate_fetch(Some(cancel_chan));
}
// The script thread associated with pipeline_id has loaded a URL in an
// iframe via script. This will result in a new pipeline being spawned and
// a child being added to the parent browsing context. This message is never

View file

@ -11,7 +11,6 @@ mod browsingcontext;
mod constellation;
mod event_loop;
mod logging;
mod network_listener;
mod pipeline;
mod sandboxing;
mod serviceworker;

View file

@ -1,192 +0,0 @@
/* 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/. */
//! The listener that encapsulates all state for an in-progress document request.
//! Any redirects that are encountered are followed. Whenever a non-redirect
//! response is received, it is forwarded to the appropriate script thread.
use base::id::PipelineId;
use crossbeam_channel::Sender;
use http::{header, HeaderMap};
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
use log::warn;
use net::http_loader::{set_default_accept_language, DOCUMENT_ACCEPT_HEADER_VALUE};
use net_traits::request::{Referrer, RequestBuilder, RequestId};
use net_traits::response::ResponseInit;
use net_traits::{
CoreResourceMsg, FetchChannels, FetchMetadata, FetchResponseMsg, IpcSend, NetworkError,
ResourceThreads,
};
pub struct NetworkListener {
res_init: Option<ResponseInit>,
request_builder: RequestBuilder,
pipeline_id: PipelineId,
resource_threads: ResourceThreads,
sender: Sender<(PipelineId, FetchResponseMsg)>,
should_send: bool,
}
impl NetworkListener {
pub fn new(
request_builder: RequestBuilder,
pipeline_id: PipelineId,
resource_threads: ResourceThreads,
sender: Sender<(PipelineId, FetchResponseMsg)>,
) -> NetworkListener {
NetworkListener {
res_init: None,
request_builder,
pipeline_id,
resource_threads,
sender,
should_send: false,
}
}
pub fn initiate_fetch(&self, cancel_chan: Option<ipc::IpcReceiver<()>>) {
let (ipc_sender, ipc_receiver) = ipc::channel().expect("Failed to create IPC channel!");
let mut listener = NetworkListener {
res_init: self.res_init.clone(),
request_builder: self.request_builder.clone(),
resource_threads: self.resource_threads.clone(),
sender: self.sender.clone(),
pipeline_id: self.pipeline_id,
should_send: false,
};
let msg = match self.res_init {
Some(ref res_init_) => CoreResourceMsg::FetchRedirect(
self.request_builder.clone(),
res_init_.clone(),
ipc_sender,
None,
),
None => {
if !listener
.request_builder
.headers
.contains_key(header::ACCEPT)
{
listener
.request_builder
.headers
.insert(header::ACCEPT, DOCUMENT_ACCEPT_HEADER_VALUE);
}
set_default_accept_language(&mut listener.request_builder.headers);
CoreResourceMsg::Fetch(
listener.request_builder.clone(),
FetchChannels::ResponseMsg(ipc_sender, cancel_chan),
)
},
};
ROUTER.add_typed_route(
ipc_receiver,
Box::new(move |message| {
match message {
Ok(FetchResponseMsg::ProcessResponse(request_id, res)) => {
listener.check_redirect(request_id, res)
},
Ok(msg_) => listener.send(msg_),
Err(e) => warn!("Error while receiving network listener message: {}", e),
};
}),
);
if let Err(e) = self.resource_threads.sender().send(msg) {
warn!("Resource thread unavailable ({})", e);
}
}
fn check_redirect(
&mut self,
request_id: RequestId,
message: Result<FetchMetadata, NetworkError>,
) {
match message {
Ok(res_metadata) => {
let metadata = match res_metadata {
FetchMetadata::Filtered { ref unsafe_, .. } => unsafe_,
FetchMetadata::Unfiltered(ref m) => m,
};
match metadata.location_url {
// https://html.spec.whatwg.org/multipage/#process-a-navigate-fetch
// Step 7-4.
Some(Ok(ref location_url))
if matches!(location_url.scheme(), "http" | "https") =>
{
if self.request_builder.url_list.is_empty() {
self.request_builder
.url_list
.push(self.request_builder.url.clone());
}
self.request_builder
.url_list
.push(metadata.final_url.clone());
self.request_builder.referrer = metadata
.referrer
.clone()
.map(Referrer::ReferrerUrl)
.unwrap_or(Referrer::NoReferrer);
self.request_builder.referrer_policy = metadata.referrer_policy;
let headers = if let Some(ref headers) = metadata.headers {
headers.clone().into_inner()
} else {
HeaderMap::new()
};
self.res_init = Some(ResponseInit {
url: metadata.final_url.clone(),
location_url: metadata.location_url.clone(),
headers,
referrer: metadata.referrer.clone(),
status_code: metadata
.status
.try_code()
.map(|code| code.as_u16())
.unwrap_or(200),
});
// XXXManishearth we don't have the cancel_chan anymore and
// can't use it here.
//
// Ideally the Fetch code would handle manual redirects on its own
self.initiate_fetch(None);
},
_ => {
// Response should be processed by script thread.
self.should_send = true;
self.send(FetchResponseMsg::ProcessResponse(
request_id,
Ok(res_metadata),
));
},
};
},
Err(e) => {
self.should_send = true;
self.send(FetchResponseMsg::ProcessResponse(request_id, Err(e)))
},
};
}
fn send(&mut self, msg: FetchResponseMsg) {
if self.should_send {
if let Err(e) = self.sender.send((self.pipeline_id, msg)) {
warn!(
"Failed to forward network message to pipeline {:?}: {:?}",
self.pipeline_id, e
);
}
}
}
}

View file

@ -150,7 +150,6 @@ mod from_script {
},
Self::ScheduleBroadcast(..) => target!("ScheduleBroadcast"),
Self::ForwardToEmbedder(msg) => msg.log_target(),
Self::InitiateNavigateRequest(..) => target!("InitiateNavigateRequest"),
Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"),
Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"),
Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"),

View file

@ -661,6 +661,7 @@ impl RemoteWebFontDownloader {
&core_resource_thread_clone,
request,
None,
None,
Box::new(move |response_message| {
match downloader.handle_web_font_fetch_message(response_message) {
DownloaderResponseResult::InProcess => {},

View file

@ -28,8 +28,8 @@ use net_traits::request::{
};
use net_traits::response::{Response, ResponseBody, ResponseType};
use net_traits::{
FetchTaskTarget, NetworkError, ReferrerPolicy, ResourceAttribute, ResourceFetchTiming,
ResourceTimeValue, ResourceTimingType,
set_default_accept_language, FetchTaskTarget, NetworkError, ReferrerPolicy, ResourceAttribute,
ResourceFetchTiming, ResourceTimeValue, ResourceTimingType,
};
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Serialize};
@ -40,10 +40,7 @@ use tokio::sync::mpsc::{UnboundedReceiver as TokioReceiver, UnboundedSender as T
use crate::fetch::cors_cache::CorsCache;
use crate::fetch::headers::determine_nosniff;
use crate::filemanager_thread::FileManager;
use crate::http_loader::{
determine_requests_referrer, http_fetch, set_default_accept, set_default_accept_language,
HttpState,
};
use crate::http_loader::{determine_requests_referrer, http_fetch, set_default_accept, HttpState};
use crate::protocols::ProtocolRegistry;
use crate::subresource_integrity::is_response_integrity_valid;

View file

@ -56,6 +56,7 @@ use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType};
use net_traits::{
CookieSource, FetchMetadata, NetworkError, RedirectEndValue, RedirectStartValue,
ReferrerPolicy, ResourceAttribute, ResourceFetchTiming, ResourceTimeValue,
DOCUMENT_ACCEPT_HEADER_VALUE,
};
use servo_arc::Arc;
use servo_url::{ImmutableOrigin, ServoUrl};
@ -77,10 +78,6 @@ use crate::hsts::HstsList;
use crate::http_cache::{CacheKey, HttpCache};
use crate::resource_thread::{AuthCache, AuthCacheEntry};
/// <https://fetch.spec.whatwg.org/#document-accept-header-value>
pub const DOCUMENT_ACCEPT_HEADER_VALUE: HeaderValue =
HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
/// The various states an entry of the HttpCache can be in.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HttpCacheEntryState {
@ -146,18 +143,6 @@ fn set_default_accept_encoding(headers: &mut HeaderMap) {
);
}
pub fn set_default_accept_language(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_LANGUAGE) {
return;
}
// TODO(eijebong): Change this once typed headers are done
headers.insert(
header::ACCEPT_LANGUAGE,
HeaderValue::from_static("en-US,en;q=0.5"),
);
}
/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-state-no-referrer-when-downgrade>
fn no_referrer_when_downgrade(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
// Step 1

View file

@ -141,6 +141,7 @@ impl DocumentLoader {
fetch_async(
&self.resource_threads.core_thread,
request,
None, /* response_init */
Some(canceller),
callback,
);

View file

@ -3262,6 +3262,7 @@ impl GlobalScope {
fetch_async(
&self.core_resource_thread(),
request_builder,
None,
cancellation_receiver,
network_listener.into_callback(),
);

View file

@ -48,6 +48,7 @@ mod mem;
#[allow(unsafe_code)]
pub(crate) mod messaging;
mod microtask;
mod navigation;
mod network_listener;
#[allow(dead_code)]
mod realms;

View file

@ -13,6 +13,7 @@ use crossbeam_channel::{select, Receiver, SendError, Sender};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg};
use ipc_channel::ipc::IpcSender;
use net_traits::image_cache::PendingImageResponse;
use net_traits::FetchResponseMsg;
use profile_traits::mem::{self as profile_mem, OpaqueSender, ReportsChan};
use profile_traits::time::{self as profile_time};
use script_traits::{ConstellationControlMsg, LayoutMsg, Painter, ScriptMsg};
@ -49,7 +50,6 @@ impl MixedMessage {
match self {
MixedMessage::FromConstellation(ref inner_msg) => match *inner_msg {
ConstellationControlMsg::StopDelayingLoadEventsMode(id) => Some(id),
ConstellationControlMsg::NavigationResponse(id, _) => Some(id),
ConstellationControlMsg::AttachLayout(ref new_layout_info) => new_layout_info
.parent_info
.or(Some(new_layout_info.new_pipeline_id)),
@ -95,6 +95,7 @@ impl MixedMessage {
pipeline_id
},
MainThreadScriptMsg::Common(CommonScriptMsg::CollectReports(_)) => None,
MainThreadScriptMsg::NavigationResponse { pipeline_id, .. } => Some(pipeline_id),
MainThreadScriptMsg::WorkletLoaded(pipeline_id) => Some(pipeline_id),
MainThreadScriptMsg::RegisterPaintWorklet { pipeline_id, .. } => Some(pipeline_id),
MainThreadScriptMsg::Inactive => None,
@ -116,6 +117,10 @@ pub(crate) enum MainThreadScriptMsg {
/// Notifies the script thread that a new worklet has been loaded, and thus the page should be
/// reflowed.
WorkletLoaded(PipelineId),
NavigationResponse {
pipeline_id: PipelineId,
message: Box<FetchResponseMsg>,
},
/// Notifies the script thread that a new paint worklet has been registered.
RegisterPaintWorklet {
pipeline_id: PipelineId,

View file

@ -0,0 +1,224 @@
/* 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/. */
//! The listener that encapsulates all state for an in-progress document request.
//! Any redirects that are encountered are followed. Whenever a non-redirect
//! response is received, it is forwarded to the appropriate script thread.
use std::cell::Cell;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{BrowsingContextId, PipelineId, TopLevelBrowsingContextId};
use content_security_policy::Destination;
use crossbeam_channel::Sender;
use http::header;
use ipc_channel::ipc;
use net_traits::request::{CredentialsMode, RedirectMode, RequestBuilder, RequestMode};
use net_traits::response::ResponseInit;
use net_traits::{
fetch_async, set_default_accept_language, BoxedFetchCallback, CoreResourceThread,
FetchResponseMsg, Metadata, DOCUMENT_ACCEPT_HEADER_VALUE,
};
use script_traits::{DocumentActivity, LoadData, WindowSizeData};
use servo_url::{MutableOrigin, ServoUrl};
use crate::fetch::FetchCanceller;
use crate::messaging::MainThreadScriptMsg;
#[derive(Clone)]
pub struct NavigationListener {
request_builder: RequestBuilder,
main_thread_sender: Sender<MainThreadScriptMsg>,
// Whether or not results are sent to the main thread. After a redirect results are no longer sent,
// as the main thread has already started a new request.
send_results_to_main_thread: Cell<bool>,
}
impl NavigationListener {
pub(crate) fn into_callback(self) -> BoxedFetchCallback {
Box::new(move |response_msg| self.notify_fetch(response_msg))
}
pub fn new(
request_builder: RequestBuilder,
main_thread_sender: Sender<MainThreadScriptMsg>,
) -> NavigationListener {
NavigationListener {
request_builder,
main_thread_sender,
send_results_to_main_thread: Cell::new(true),
}
}
pub fn initiate_fetch(
self,
core_resource_thread: &CoreResourceThread,
response_init: Option<ResponseInit>,
cancellation_receiver: Option<ipc::IpcReceiver<()>>,
) {
fetch_async(
core_resource_thread,
self.request_builder.clone(),
response_init,
cancellation_receiver,
self.into_callback(),
);
}
fn notify_fetch(&self, message: FetchResponseMsg) {
// If we've already asked the main thread to redirect the response, then stop sending results
// for this fetch. The main thread has already replaced it.
if !self.send_results_to_main_thread.get() {
return;
}
// If this is a redirect, don't send any more message after this one.
if Self::http_redirect_metadata(&message).is_some() {
self.send_results_to_main_thread.set(false);
}
let pipeline_id = self
.request_builder
.pipeline_id
.expect("Navigation should always have an associated Pipeline");
let result = self
.main_thread_sender
.send(MainThreadScriptMsg::NavigationResponse {
pipeline_id,
message: Box::new(message),
});
if let Err(error) = result {
warn!(
"Failed to send network message to pipeline {:?}: {error:?}",
pipeline_id
);
}
}
pub(crate) fn http_redirect_metadata(message: &FetchResponseMsg) -> Option<&Metadata> {
let FetchResponseMsg::ProcessResponse(_, Ok(ref metadata)) = message else {
return None;
};
// Don't allow redirects for non HTTP(S) URLs.
let metadata = metadata.metadata();
if !matches!(
metadata.location_url,
Some(Ok(ref location_url)) if matches!(location_url.scheme(), "http" | "https")
) {
return None;
}
Some(metadata)
}
}
/// A document load that is in the process of fetching the requested resource. Contains
/// data that will need to be present when the document and frame tree entry are created,
/// but is only easily available at initiation of the load and on a push basis (so some
/// data will be updated according to future resize events, viewport changes, etc.)
#[derive(JSTraceable)]
pub(crate) struct InProgressLoad {
/// The pipeline which requested this load.
#[no_trace]
pub(crate) pipeline_id: PipelineId,
/// The browsing context being loaded into.
#[no_trace]
pub(crate) browsing_context_id: BrowsingContextId,
/// The top level ancestor browsing context.
#[no_trace]
pub(crate) top_level_browsing_context_id: TopLevelBrowsingContextId,
/// The parent pipeline and frame type associated with this load, if any.
#[no_trace]
pub(crate) parent_info: Option<PipelineId>,
/// The opener, if this is an auxiliary.
#[no_trace]
pub(crate) opener: Option<BrowsingContextId>,
/// The current window size associated with this pipeline.
#[no_trace]
pub(crate) window_size: WindowSizeData,
/// The activity level of the document (inactive, active or fully active).
#[no_trace]
pub(crate) activity: DocumentActivity,
/// Window is throttled, running timers at a heavily limited rate.
pub(crate) throttled: bool,
/// The origin for the document
#[no_trace]
pub(crate) origin: MutableOrigin,
/// Timestamp reporting the time when the browser started this load.
#[no_trace]
pub(crate) navigation_start: CrossProcessInstant,
/// For cancelling the fetch
pub(crate) canceller: FetchCanceller,
/// The [`LoadData`] associated with this load.
#[no_trace]
pub(crate) load_data: LoadData,
/// A list of URL to keep track of all the redirects that have happened during
/// this load.
#[no_trace]
pub(crate) url_list: Vec<ServoUrl>,
}
impl InProgressLoad {
/// Create a new InProgressLoad object.
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
id: PipelineId,
browsing_context_id: BrowsingContextId,
top_level_browsing_context_id: TopLevelBrowsingContextId,
parent_info: Option<PipelineId>,
opener: Option<BrowsingContextId>,
window_size: WindowSizeData,
origin: MutableOrigin,
load_data: LoadData,
) -> InProgressLoad {
let url = load_data.url.clone();
InProgressLoad {
pipeline_id: id,
browsing_context_id,
top_level_browsing_context_id,
parent_info,
opener,
window_size,
activity: DocumentActivity::FullyActive,
throttled: false,
origin,
navigation_start: CrossProcessInstant::now(),
canceller: Default::default(),
load_data,
url_list: vec![url],
}
}
pub(crate) fn request_builder(&mut self) -> RequestBuilder {
let id = self.pipeline_id;
let top_level_browsing_context_id = self.top_level_browsing_context_id;
let mut request_builder =
RequestBuilder::new(self.load_data.url.clone(), self.load_data.referrer.clone())
.method(self.load_data.method.clone())
.destination(Destination::Document)
.mode(RequestMode::Navigate)
.credentials_mode(CredentialsMode::Include)
.use_url_credentials(true)
.pipeline_id(Some(id))
.target_browsing_context_id(Some(top_level_browsing_context_id))
.referrer_policy(self.load_data.referrer_policy)
.headers(self.load_data.headers.clone())
.body(self.load_data.data.clone())
.redirect_mode(RedirectMode::Manual)
.origin(self.origin.immutable().clone())
.crash(self.load_data.crash.clone());
request_builder.url_list = self.url_list.clone();
if !request_builder.headers.contains_key(header::ACCEPT) {
request_builder
.headers
.insert(header::ACCEPT, DOCUMENT_ACCEPT_HEADER_VALUE);
}
set_default_accept_language(&mut request_builder.headers);
request_builder
}
}

View file

@ -63,9 +63,8 @@ use media::WindowGLContext;
use metrics::{PaintTimeMetrics, MAX_TASK_NS};
use mime::{self, Mime};
use net_traits::image_cache::{ImageCache, PendingImageResponse};
use net_traits::request::{
CredentialsMode, Destination, RedirectMode, RequestBuilder, RequestId, RequestMode,
};
use net_traits::request::{Referrer, RequestId};
use net_traits::response::ResponseInit;
use net_traits::storage_thread::StorageType;
use net_traits::{
FetchMetadata, FetchResponseListener, FetchResponseMsg, Metadata, NetworkError,
@ -140,12 +139,12 @@ use crate::dom::window::Window;
use crate::dom::windowproxy::{CreatorBrowsingContextInfo, WindowProxy};
use crate::dom::worklet::WorkletThreadPool;
use crate::dom::workletglobalscope::WorkletGlobalScopeInit;
use crate::fetch::FetchCanceller;
use crate::messaging::{
CommonScriptMsg, MainThreadScriptMsg, MixedMessage, ScriptEventLoopSender,
ScriptThreadReceivers, ScriptThreadSenders,
};
use crate::microtask::{Microtask, MicrotaskQueue};
use crate::navigation::{InProgressLoad, NavigationListener};
use crate::realms::enter_realm;
use crate::script_module::ScriptFetchOptions;
use crate::script_runtime::{
@ -181,83 +180,6 @@ pub(crate) unsafe fn trace_thread(tr: *mut JSTracer) {
})
}
/// A document load that is in the process of fetching the requested resource. Contains
/// data that will need to be present when the document and frame tree entry are created,
/// but is only easily available at initiation of the load and on a push basis (so some
/// data will be updated according to future resize events, viewport changes, etc.)
#[derive(JSTraceable)]
struct InProgressLoad {
/// The pipeline which requested this load.
#[no_trace]
pipeline_id: PipelineId,
/// The browsing context being loaded into.
#[no_trace]
browsing_context_id: BrowsingContextId,
/// The top level ancestor browsing context.
#[no_trace]
top_level_browsing_context_id: TopLevelBrowsingContextId,
/// The parent pipeline and frame type associated with this load, if any.
#[no_trace]
parent_info: Option<PipelineId>,
/// The opener, if this is an auxiliary.
#[no_trace]
opener: Option<BrowsingContextId>,
/// The current window size associated with this pipeline.
#[no_trace]
window_size: WindowSizeData,
/// The activity level of the document (inactive, active or fully active).
#[no_trace]
activity: DocumentActivity,
/// Window is throttled, running timers at a heavily limited rate.
throttled: bool,
/// The requested URL of the load.
#[no_trace]
url: ServoUrl,
/// The origin for the document
#[no_trace]
origin: MutableOrigin,
/// Timestamp reporting the time when the browser started this load.
#[no_trace]
navigation_start: CrossProcessInstant,
/// For cancelling the fetch
canceller: FetchCanceller,
/// If inheriting the security context
inherited_secure_context: Option<bool>,
}
impl InProgressLoad {
/// Create a new InProgressLoad object.
#[allow(clippy::too_many_arguments)]
fn new(
id: PipelineId,
browsing_context_id: BrowsingContextId,
top_level_browsing_context_id: TopLevelBrowsingContextId,
parent_info: Option<PipelineId>,
opener: Option<BrowsingContextId>,
window_size: WindowSizeData,
url: ServoUrl,
origin: MutableOrigin,
inherited_secure_context: Option<bool>,
) -> InProgressLoad {
let navigation_start = CrossProcessInstant::now();
InProgressLoad {
pipeline_id: id,
browsing_context_id,
top_level_browsing_context_id,
parent_info,
opener,
window_size,
activity: DocumentActivity::FullyActive,
throttled: false,
url,
origin,
navigation_start,
canceller: Default::default(),
inherited_secure_context,
}
}
}
// We borrow the incomplete parser contexts mutably during parsing,
// which is fine except that parsing can trigger evaluation,
// which can trigger GC, and so we can end up tracing the script
@ -493,7 +415,6 @@ impl ScriptThreadFactory for ScriptThread {
let top_level_browsing_context_id = state.top_level_browsing_context_id;
let parent_info = state.parent_info;
let opener = state.opener;
let secure = load_data.inherited_secure_context;
let memory_profiler_sender = state.memory_profiler_sender.clone();
let window_size = state.window_size;
@ -507,18 +428,16 @@ impl ScriptThreadFactory for ScriptThread {
let mut failsafe = ScriptMemoryFailsafe::new(&script_thread);
let origin = MutableOrigin::new(load_data.url.origin());
let new_load = InProgressLoad::new(
script_thread.pre_page_load(InProgressLoad::new(
id,
browsing_context_id,
top_level_browsing_context_id,
parent_info,
opener,
window_size,
load_data.url.clone(),
origin,
secure,
);
script_thread.pre_page_load(new_load, load_data);
load_data,
));
let reporter_name = format!("script-reporter-{:?}", id);
memory_profiler_sender.run_with_memory_reporting(
@ -1822,20 +1741,6 @@ impl ScriptThread {
ConstellationControlMsg::StopDelayingLoadEventsMode(pipeline_id) => {
self.handle_stop_delaying_load_events_mode(pipeline_id)
},
ConstellationControlMsg::NavigationResponse(pipeline_id, fetch_data) => {
match fetch_data {
FetchResponseMsg::ProcessResponse(request_id, metadata) => {
self.handle_fetch_metadata(pipeline_id, request_id, metadata)
},
FetchResponseMsg::ProcessResponseChunk(request_id, chunk) => {
self.handle_fetch_chunk(pipeline_id, request_id, chunk)
},
FetchResponseMsg::ProcessResponseEOF(request_id, eof) => {
self.handle_fetch_eof(pipeline_id, request_id, eof)
},
_ => unreachable!(),
};
},
ConstellationControlMsg::NavigateIframe(
parent_pipeline_id,
browsing_context_id,
@ -2079,6 +1984,12 @@ impl ScriptThread {
MainThreadScriptMsg::Common(CommonScriptMsg::CollectReports(chan)) => {
self.collect_reports(chan)
},
MainThreadScriptMsg::NavigationResponse {
pipeline_id,
message,
} => {
self.handle_navigation_response(pipeline_id, *message);
},
MainThreadScriptMsg::WorkletLoaded(pipeline_id) => {
self.handle_worklet_loaded(pipeline_id)
},
@ -2484,6 +2395,7 @@ impl ScriptThread {
} = new_layout_info;
// Kick off the fetch for the new resource.
let url = load_data.url.clone();
let new_load = InProgressLoad::new(
new_pipeline_id,
browsing_context_id,
@ -2491,16 +2403,15 @@ impl ScriptThread {
parent_info,
opener,
window_size,
load_data.url.clone(),
origin,
load_data.inherited_secure_context,
load_data,
);
if load_data.url.as_str() == "about:blank" {
self.start_page_load_about_blank(new_load, load_data);
} else if load_data.url.as_str() == "about:srcdoc" {
self.page_load_about_srcdoc(new_load, load_data);
if url.as_str() == "about:blank" {
self.start_page_load_about_blank(new_load);
} else if url.as_str() == "about:srcdoc" {
self.page_load_about_srcdoc(new_load);
} else {
self.pre_page_load(new_load, load_data);
self.pre_page_load(new_load);
}
}
@ -3118,7 +3029,7 @@ impl ScriptThread {
}
debug!(
"ScriptThread: loading {} on pipeline {:?}",
incomplete.url, incomplete.pipeline_id
incomplete.load_data.url, incomplete.pipeline_id
);
let origin = if final_url.as_str() == "about:blank" || final_url.as_str() == "about:srcdoc"
@ -3201,7 +3112,7 @@ impl ScriptThread {
self.player_context.clone(),
#[cfg(feature = "webgpu")]
self.gpu_id_hub.clone(),
incomplete.inherited_secure_context,
incomplete.load_data.inherited_secure_context,
);
let _realm = enter_realm(&*window);
@ -3508,42 +3419,48 @@ impl ScriptThread {
/// Instructs the constellation to fetch the document that will be loaded. Stores the InProgressLoad
/// argument until a notification is received that the fetch is complete.
fn pre_page_load(&self, mut incomplete: InProgressLoad, load_data: LoadData) {
let id = incomplete.pipeline_id;
let top_level_browsing_context_id = incomplete.top_level_browsing_context_id;
let req_init = RequestBuilder::new(load_data.url.clone(), load_data.referrer)
.method(load_data.method)
.destination(Destination::Document)
.mode(RequestMode::Navigate)
.credentials_mode(CredentialsMode::Include)
.use_url_credentials(true)
.pipeline_id(Some(id))
.target_browsing_context_id(Some(top_level_browsing_context_id))
.referrer_policy(load_data.referrer_policy)
.headers(load_data.headers)
.body(load_data.data)
.redirect_mode(RedirectMode::Manual)
.origin(incomplete.origin.immutable().clone())
.crash(load_data.crash);
let context = ParserContext::new(id, load_data.url);
fn pre_page_load(&self, mut incomplete: InProgressLoad) {
let context = ParserContext::new(incomplete.pipeline_id, incomplete.load_data.url.clone());
self.incomplete_parser_contexts
.0
.borrow_mut()
.push((id, context));
.push((incomplete.pipeline_id, context));
let cancel_chan = incomplete.canceller.initialize();
let cancellation_receiver = incomplete.canceller.initialize();
NavigationListener::new(
incomplete.request_builder(),
self.senders.self_sender.clone(),
)
.initiate_fetch(
&self.resource_threads.core_thread,
None,
Some(cancellation_receiver),
);
self.senders
.pipeline_to_constellation_sender
.send((
id,
ScriptMsg::InitiateNavigateRequest(req_init, cancel_chan),
))
.unwrap();
self.incomplete_loads.borrow_mut().push(incomplete);
}
fn handle_navigation_response(&self, pipeline_id: PipelineId, message: FetchResponseMsg) {
if let Some(metadata) = NavigationListener::http_redirect_metadata(&message) {
self.handle_navigation_redirect(pipeline_id, metadata);
return;
};
match message {
FetchResponseMsg::ProcessResponse(request_id, metadata) => {
self.handle_fetch_metadata(pipeline_id, request_id, metadata)
},
FetchResponseMsg::ProcessResponseChunk(request_id, chunk) => {
self.handle_fetch_chunk(pipeline_id, request_id, chunk)
},
FetchResponseMsg::ProcessResponseEOF(request_id, eof) => {
self.handle_fetch_eof(pipeline_id, request_id, eof)
},
FetchResponseMsg::ProcessRequestBody(..) => {},
FetchResponseMsg::ProcessRequestEOF(..) => {},
}
}
fn handle_fetch_metadata(
&self,
id: PipelineId,
@ -3596,24 +3513,74 @@ impl ScriptThread {
}
}
fn handle_navigation_redirect(&self, id: PipelineId, metadata: &Metadata) {
// TODO(mrobinson): This tries to accomplish some steps from
// <https://html.spec.whatwg.org/multipage/#process-a-navigate-fetch>, but it's
// very out of sync with the specification.
assert!(metadata.location_url.is_some());
let mut incomplete_loads = self.incomplete_loads.borrow_mut();
let Some(incomplete_load) = incomplete_loads
.iter_mut()
.find(|incomplete_load| incomplete_load.pipeline_id == id)
else {
return;
};
// Update the `url_list` of the incomplete load to track all redirects. This will be reflected
// in the new `RequestBuilder` as well.
incomplete_load.url_list.push(metadata.final_url.clone());
let mut request_builder = incomplete_load.request_builder();
request_builder.referrer = metadata
.referrer
.clone()
.map(Referrer::ReferrerUrl)
.unwrap_or(Referrer::NoReferrer);
request_builder.referrer_policy = metadata.referrer_policy;
let headers = metadata
.headers
.as_ref()
.map(|headers| headers.clone().into_inner())
.unwrap_or_default();
let response_init = Some(ResponseInit {
url: metadata.final_url.clone(),
location_url: metadata.location_url.clone(),
headers,
referrer: metadata.referrer.clone(),
status_code: metadata
.status
.try_code()
.map(|code| code.as_u16())
.unwrap_or(200),
});
let cancellation_receiver = incomplete_load.canceller.initialize();
NavigationListener::new(request_builder, self.senders.self_sender.clone()).initiate_fetch(
&self.resource_threads.core_thread,
response_init,
Some(cancellation_receiver),
);
}
/// Synchronously fetch `about:blank`. Stores the `InProgressLoad`
/// argument until a notification is received that the fetch is complete.
fn start_page_load_about_blank(&self, incomplete: InProgressLoad, load_data: LoadData) {
fn start_page_load_about_blank(&self, mut incomplete: InProgressLoad) {
let id = incomplete.pipeline_id;
self.incomplete_loads.borrow_mut().push(incomplete);
let url = ServoUrl::parse("about:blank").unwrap();
let mut context = ParserContext::new(id, url.clone());
let mut meta = Metadata::default(url);
meta.set_content_type(Some(&mime::TEXT_HTML));
meta.set_referrer_policy(load_data.referrer_policy);
meta.set_referrer_policy(incomplete.load_data.referrer_policy);
// If this page load is the result of a javascript scheme url, map
// the evaluation result into a response.
let chunk = match load_data.js_eval_result {
Some(JsEvalResult::Ok(content)) => content,
let chunk = match incomplete.load_data.js_eval_result {
Some(JsEvalResult::Ok(ref mut content)) => std::mem::take(content),
Some(JsEvalResult::NoContent) => {
meta.status = http::StatusCode::NO_CONTENT.into();
vec![]
@ -3621,6 +3588,8 @@ impl ScriptThread {
None => vec![],
};
self.incomplete_loads.borrow_mut().push(incomplete);
let dummy_request_id = RequestId::next();
context.process_response(dummy_request_id, Ok(FetchMetadata::Unfiltered(meta)));
context.process_response_chunk(dummy_request_id, chunk);
@ -3631,20 +3600,20 @@ impl ScriptThread {
}
/// Synchronously parse a srcdoc document from a giving HTML string.
fn page_load_about_srcdoc(&self, incomplete: InProgressLoad, load_data: LoadData) {
fn page_load_about_srcdoc(&self, mut incomplete: InProgressLoad) {
let id = incomplete.pipeline_id;
let url = ServoUrl::parse("about:srcdoc").unwrap();
let mut meta = Metadata::default(url.clone());
meta.set_content_type(Some(&mime::TEXT_HTML));
meta.set_referrer_policy(incomplete.load_data.referrer_policy);
let srcdoc = std::mem::take(&mut incomplete.load_data.srcdoc);
let chunk = srcdoc.into_bytes();
self.incomplete_loads.borrow_mut().push(incomplete);
let url = ServoUrl::parse("about:srcdoc").unwrap();
let mut context = ParserContext::new(id, url.clone());
let mut meta = Metadata::default(url);
meta.set_content_type(Some(&mime::TEXT_HTML));
meta.set_referrer_policy(load_data.referrer_policy);
let chunk = load_data.srcdoc.into_bytes();
let mut context = ParserContext::new(id, url);
let dummy_request_id = RequestId::next();
context.process_response(dummy_request_id, Ok(FetchMetadata::Unfiltered(meta)));
context.process_response_chunk(dummy_request_id, chunk);

View file

@ -14,7 +14,7 @@ use base::id::HistoryStateId;
use cookie::Cookie;
use crossbeam_channel::{unbounded, Receiver, Sender};
use headers::{ContentType, HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
use http::{Error as HttpError, HeaderMap, StatusCode};
use http::{header, Error as HttpError, HeaderMap, HeaderValue, StatusCode};
use hyper_serde::Serde;
use hyper_util::client::legacy::Error as HyperError;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@ -46,6 +46,10 @@ pub mod request;
pub mod response;
pub mod storage_thread;
/// <https://fetch.spec.whatwg.org/#document-accept-header-value>
pub const DOCUMENT_ACCEPT_HEADER_VALUE: HeaderValue =
HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
/// An implementation of the [Fetch specification](https://fetch.spec.whatwg.org/)
pub mod fetch {
pub mod headers;
@ -197,7 +201,7 @@ pub enum FetchResponseMsg {
}
impl FetchResponseMsg {
fn request_id(&self) -> RequestId {
pub fn request_id(&self) -> RequestId {
match self {
FetchResponseMsg::ProcessRequestBody(id) |
FetchResponseMsg::ProcessRequestEOF(id) |
@ -252,6 +256,15 @@ pub enum FetchMetadata {
},
}
impl FetchMetadata {
pub fn metadata(&self) -> &Metadata {
match self {
Self::Unfiltered(metadata) => metadata,
Self::Filtered { unsafe_, .. } => unsafe_,
}
}
}
pub trait FetchResponseListener {
fn process_request_body(&mut self, request_id: RequestId);
fn process_request_eof(&mut self, request_id: RequestId);
@ -511,6 +524,7 @@ pub enum CoreResourceMsg {
enum ToFetchThreadMessage {
StartFetch(
/* request_builder */ RequestBuilder,
/* response_init */ Option<ResponseInit>,
/* cancel_chan */ Option<IpcReceiver<()>>,
/* callback */ BoxedFetchCallback,
),
@ -570,14 +584,29 @@ impl FetchThread {
fn run(&mut self) {
loop {
match self.receiver.recv().unwrap() {
ToFetchThreadMessage::StartFetch(request_builder, canceller, callback) => {
ToFetchThreadMessage::StartFetch(
request_builder,
response_init,
canceller,
callback,
) => {
self.active_fetches.insert(request_builder.id, callback);
self.core_resource_thread
.send(CoreResourceMsg::Fetch(
// Only redirects have a `response_init` field.
let message = match response_init {
Some(response_init) => CoreResourceMsg::FetchRedirect(
request_builder,
response_init,
self.to_fetch_sender.clone(),
canceller,
),
None => CoreResourceMsg::Fetch(
request_builder,
FetchChannels::ResponseMsg(self.to_fetch_sender.clone(), canceller),
))
.unwrap();
),
};
self.core_resource_thread.send(message).unwrap();
},
ToFetchThreadMessage::FetchResponse(fetch_response_msg) => {
let request_id = fetch_response_msg.request_id();
@ -599,18 +628,23 @@ impl FetchThread {
}
}
/// Instruct the resource thread to make a new request.
static FETCH_THREAD: OnceLock<Sender<ToFetchThreadMessage>> = OnceLock::new();
/// Instruct the resource thread to make a new fetch request.
pub fn fetch_async(
core_resource_thread: &CoreResourceThread,
request: RequestBuilder,
response_init: Option<ResponseInit>,
canceller: Option<IpcReceiver<()>>,
callback: BoxedFetchCallback,
) {
static FETCH_THREAD: OnceLock<Sender<ToFetchThreadMessage>> = OnceLock::new();
let _ = FETCH_THREAD
.get_or_init(|| FetchThread::spawn(core_resource_thread))
.send(ToFetchThreadMessage::StartFetch(
request, canceller, callback,
request,
response_init,
canceller,
callback,
));
}
@ -945,5 +979,17 @@ pub fn http_percent_encode(bytes: &[u8]) -> String {
percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string()
}
pub fn set_default_accept_language(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_LANGUAGE) {
return;
}
// TODO(eijebong): Change this once typed headers are done
headers.insert(
header::ACCEPT_LANGUAGE,
HeaderValue::from_static("en-US,en;q=0.5"),
);
}
pub static PRIVILEGED_SECRET: LazyLock<u32> =
LazyLock::new(|| servo_rand::ServoRng::default().next_u32());

View file

@ -47,7 +47,7 @@ use media::WindowGLContext;
use net_traits::image_cache::ImageCache;
use net_traits::request::{Referrer, RequestBody};
use net_traits::storage_thread::StorageType;
use net_traits::{FetchResponseMsg, ReferrerPolicy, ResourceThreads};
use net_traits::{ReferrerPolicy, ResourceThreads};
use pixels::{Image, PixelFormat};
use profile_traits::{mem, time as profile_time};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@ -286,16 +286,12 @@ pub enum UpdatePipelineIdReason {
/// Messages sent from the constellation or layout to the script thread.
// FIXME: https://github.com/servo/servo/issues/34591
#[expect(clippy::large_enum_variant)]
#[derive(Deserialize, Serialize)]
pub enum ConstellationControlMsg {
/// Takes the associated window proxy out of "delaying-load-events-mode",
/// used if a scheduled navigated was refused by the embedder.
/// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
StopDelayingLoadEventsMode(PipelineId),
/// Sends the final response to script thread for fetching after all redirections
/// have been resolved
NavigationResponse(PipelineId, FetchResponseMsg),
/// Gives a channel and ID to a layout, as well as the ID of that layout's parent
AttachLayout(NewLayoutInfo),
/// Window resized. Sends a DOM event eventually, but first we combine events.
@ -413,7 +409,6 @@ impl fmt::Debug for ConstellationControlMsg {
use self::ConstellationControlMsg::*;
let variant = match *self {
StopDelayingLoadEventsMode(..) => "StopDelayingLoadsEventMode",
NavigationResponse(..) => "NavigationResponse",
AttachLayout(..) => "AttachLayout",
Resize(..) => "Resize",
ThemeChange(..) => "ThemeChange",

View file

@ -17,7 +17,6 @@ use embedder_traits::{EmbedderMsg, MediaSessionEvent};
use euclid::default::Size2D as UntypedSize2D;
use euclid::Size2D;
use ipc_channel::ipc::{IpcReceiver, IpcSender};
use net_traits::request::RequestBuilder;
use net_traits::storage_thread::StorageType;
use net_traits::CoreResourceMsg;
use serde::{Deserialize, Serialize};
@ -128,9 +127,6 @@ pub enum ScriptMsg {
ScheduleBroadcast(BroadcastChannelRouterId, BroadcastMsg),
/// Forward a message to the embedder.
ForwardToEmbedder(EmbedderMsg),
/// Requests are sent to constellation and fetches are checked manually
/// for cross-origin loads
InitiateNavigateRequest(RequestBuilder, /* cancellation_chan */ IpcReceiver<()>),
/// Broadcast a storage event to every same-origin pipeline.
/// The strings are key, old value and new value.
BroadcastStorageEvent(
@ -271,7 +267,6 @@ impl fmt::Debug for ScriptMsg {
NewBroadcastChannelNameInRouter(..) => "NewBroadcastChannelNameInRouter",
ScheduleBroadcast(..) => "ScheduleBroadcast",
ForwardToEmbedder(..) => "ForwardToEmbedder",
InitiateNavigateRequest(..) => "InitiateNavigateRequest",
BroadcastStorageEvent(..) => "BroadcastStorageEvent",
ChangeRunningAnimationsState(..) => "ChangeRunningAnimationsState",
CreateCanvasPaintThread(..) => "CreateCanvasPaintThread",