mirror of
https://github.com/lexmount/moli.git
synced 2026-11-04 00:06:51 +00:00
fix(preload): consume document-owned preload responses
This commit is contained in:
@@ -91,6 +91,7 @@ async fn external_raw_bridge_drop_after_body_eof_cancels_pending_fetch_completio
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_rx,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
async function () {
|
||||
return (async()=>{
|
||||
const controlled=navigator.serviceWorker.controller?Promise.resolve():new Promise(resolve=>navigator.serviceWorker.addEventListener('controllerchange',resolve,{once:true}));
|
||||
await navigator.serviceWorker.register('/probe-worker.js');await navigator.serviceWorker.ready;await controlled;
|
||||
const rows=[];
|
||||
for(const destination of ['script','fetch']) for(const source of ['basic','cors','default','opaque']) for(const bad of [false,true]){
|
||||
const token='sw'+rows.length,url=new URL('/probe-worker-asset',location.href);Object.entries({source,token,as:'script'}).forEach(([k,v])=>url.searchParams.set(k,v));
|
||||
await fetch('/probe-release?token='+token);
|
||||
const link=document.createElement('link');Object.assign(link,{rel:'preload',as:destination,href:url.href,integrity:bad?'sha384-AAAA':''});
|
||||
const linkEvent=await new Promise(resolve=>{link.onload=link.onerror=e=>resolve(e.type);document.head.append(link)});
|
||||
let consumer;
|
||||
if(destination==='fetch'){
|
||||
try{const response=await fetch(url,{mode:'no-cors',credentials:'include'});consumer={event:'load',type:response.type,text:await response.text(),status:response.status};}
|
||||
catch(e){consumer={event:'error',name:e.name};}
|
||||
}else{
|
||||
const script=document.createElement('script');script.src=url;
|
||||
consumer=await new Promise(resolve=>{script.onload=script.onerror=e=>resolve({event:e.type});document.body.append(script)});script.remove();
|
||||
}
|
||||
const counts=await(await fetch('/probe-worker-counts')).json();
|
||||
rows.push({destination,source,bad,linkEvent,consumer,events:counts[url.href],timing:performance.getEntriesByName(url.href).map(e=>e.initiatorType)});link.remove();
|
||||
}
|
||||
return {rows,failures:rows.filter(r=>r.events!==1||r.linkEvent!==(r.bad?'error':'load')||r.consumer.event!==(r.bad?'error':'load')||r.timing.length!==1 ||(!r.bad&&r.destination==='fetch'&&r.source==='opaque'&&(r.consumer.type!=='opaque'||r.consumer.text!==''||r.consumer.status!==0)))};
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
self.addEventListener('install', e=>e.waitUntil(self.skipWaiting()));
|
||||
self.addEventListener('activate', e=>e.waitUntil(clients.claim()));
|
||||
const counts={};
|
||||
self.addEventListener('fetch', e=>{
|
||||
const u=new URL(e.request.url);
|
||||
if(u.pathname==='/probe-worker-counts'){e.respondWith(new Response(JSON.stringify(counts),{headers:{'Content-Type':'application/json'}}));return;}
|
||||
if(u.pathname!=='/probe-worker-asset')return;
|
||||
counts[u.href]=(counts[u.href]||0)+1;
|
||||
const kind=u.searchParams.get('source');
|
||||
const target=new URL('/probe-asset'+u.search,self.location.href);
|
||||
if(kind==='cors'||kind==='opaque')target.hostname=target.hostname==='localhost'?'127.0.0.1':'localhost';
|
||||
e.respondWith(kind==='default'?new Response('globalThis.preloadExecuted=(globalThis.preloadExecuted||0)+1;',{headers:{'Content-Type':'text/javascript'}}):fetch(target,{mode:kind==='opaque'?'no-cors':'cors'}));
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
async function () {
|
||||
const basic = await (async () => {return (async () => {
|
||||
const rows=[];
|
||||
for (const as of ['script','image']) for (const cache of ['max-age=60','no-store'])
|
||||
for (const phase of ['pending','complete']) for (const cors of [null,'anonymous','use-credentials']) {
|
||||
const token=String(rows.length), url=new URL('/probe-asset',location.href);
|
||||
url.searchParams.set('token',token);url.searchParams.set('as',as);url.searchParams.set('cache',cache);
|
||||
const link=document.createElement('link');Object.assign(link,{rel:'preload',as,href:url.href});
|
||||
if(cors!==null)link.crossOrigin=cors;
|
||||
const linkDone=new Promise(resolve=>{link.onload=link.onerror=e=>resolve(e.type)});
|
||||
document.head.append(link);
|
||||
await fetch('/probe-started?token='+token);
|
||||
if(phase==='complete'){await fetch('/probe-release?token='+token);await linkDone;}
|
||||
const consumer=document.createElement(as==='script'?'script':'img');
|
||||
if(cors!==null)consumer.crossOrigin=cors;
|
||||
let timingAtConsumer;
|
||||
const consumerDone=new Promise(resolve=>{consumer.onload=consumer.onerror=e=>{timingAtConsumer=performance.getEntriesByName(url.href).length;resolve(e.type)}});
|
||||
consumer.src=url.href;document.body.append(consumer);
|
||||
if(phase==='pending')await fetch('/probe-release?token='+token);
|
||||
const events=await Promise.all([linkDone,consumerDone]);
|
||||
const actual=await (await fetch('/probe-stats?token='+token)).json();
|
||||
rows.push({as,cache,phase,cors,events,timingAtConsumer,...actual,timing:performance.getEntriesByName(url.href).map(e=>({initiator:e.initiatorType,transfer:e.transferSize,encoded:e.encodedBodySize}))});
|
||||
link.remove();consumer.remove();
|
||||
}
|
||||
return {rows,executions:globalThis.preloadExecuted||0,failures:rows.filter(r=>r.count!==1 || r.timing.length!==1 || r.timingAtConsumer!==1 || r.events.some(e=>e!=='load'))};
|
||||
})();
|
||||
})();
|
||||
const integrity = await (async () => {return (async () => {
|
||||
const rows=[];
|
||||
for (const cache of ['max-age=60','no-store']) for (const phase of ['pending','complete'])
|
||||
for (const [name,preloadIntegrity,consumerIntegrity,preloadEvent,consumerEvent,requests] of [["good-empty", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "", "load", "load", 1], ["bad-empty", "sha384-AAAA", "", "error", "error", 1], ["bad-same", "sha384-AAAA", "sha384-AAAA", "error", "error", 1], ["bad-good", "sha384-AAAA", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "error", "load", 2], ["good-bad", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha384-AAAA", "load", "error", 2], ["good-options", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ?ignored=option", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 1], ["good-different-algorithm", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha256-cRlncoZ/tNV1cbKTR2cXiKEwVFkjvOz4kToIRLKBiSE=", "load", "load", 2], ["empty-good", "", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 2]]) {
|
||||
const token='sri-'+rows.length,url=new URL('/probe-asset',location.href);
|
||||
url.searchParams.set('token',token);url.searchParams.set('as','script');url.searchParams.set('cache',cache);
|
||||
const link=document.createElement('link');Object.assign(link,{rel:'preload',as:'script',href:url.href,integrity:preloadIntegrity});
|
||||
const linkDone=new Promise(resolve=>{link.onload=link.onerror=e=>resolve(e.type)});document.head.append(link);
|
||||
await fetch('/probe-started?token='+token);
|
||||
if(phase==='complete'){await fetch('/probe-release?token='+token);await linkDone;}
|
||||
const consumer=document.createElement('script');consumer.integrity=consumerIntegrity;consumer.src=url.href;
|
||||
const consumerDone=new Promise(resolve=>{consumer.onload=consumer.onerror=e=>resolve(e.type)});document.body.append(consumer);
|
||||
if(phase==='pending')await fetch('/probe-release?token='+token);
|
||||
const events=await Promise.all([linkDone,consumerDone]);
|
||||
const actual=await(await fetch('/probe-stats?token='+token)).json();
|
||||
rows.push({name,cache,phase,events,expectedEvents:[preloadEvent,consumerEvent],expectedRequests:requests,...actual,timing:performance.getEntriesByName(url.href).map(e=>({initiator:e.initiatorType,transfer:e.transferSize}))});
|
||||
link.remove();consumer.remove();
|
||||
}
|
||||
return {rows,failures:rows.filter(r=>JSON.stringify(r.events)!==JSON.stringify(r.expectedEvents)||r.timing.length!==r.expectedRequests || (r.cache==='no-store' && r.count!==r.expectedRequests) || (r.expectedRequests===1 && r.count!==1))};
|
||||
})();})();
|
||||
const oneShot = await (async () => {
|
||||
const url = new URL('/probe-asset?token=one-shot&as=script', location.href);
|
||||
await fetch('/probe-release?token=one-shot');
|
||||
const link = document.createElement('link');
|
||||
Object.assign(link, {rel:'preload', as:'fetch', crossOrigin:'anonymous', href:url.href});
|
||||
await new Promise(resolve => { link.onload=link.onerror=resolve; document.head.append(link); });
|
||||
const count = async () => (await (await fetch('/probe-stats?token=one-shot')).json()).count;
|
||||
const counts = [await count()];
|
||||
await (await fetch(url, {headers:{Accept:'text/javascript'}})).text(); counts.push(await count());
|
||||
await (await fetch(url)).text(); counts.push(await count());
|
||||
await (await fetch(url)).text(); counts.push(await count());
|
||||
link.remove();
|
||||
return counts;
|
||||
})();
|
||||
return {basic,integrity,oneShot};
|
||||
}
|
||||
@@ -6790,8 +6790,8 @@ async fn parse_time_lifecycle_queue_can_stop_cleanly_at_load_stage_after_load_mi
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[path = "scripts/document_write_errors.rs"]
|
||||
mod document_write_errors;
|
||||
|
||||
#[path = "scripts/child_markup_insertion.rs"]
|
||||
mod child_markup_insertion;
|
||||
#[path = "scripts/preload_consumption.rs"]
|
||||
mod preload_consumption;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use super::*;
|
||||
use anyhow::Context;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpListener,
|
||||
sync::Notify,
|
||||
task::{JoinHandle, JoinSet},
|
||||
};
|
||||
|
||||
const SCRIPT: &str = "globalThis.preloadExecuted=(globalThis.preloadExecuted||0)+1;";
|
||||
const PROBE: &str = include_str!("../fixtures/preload-consumption.js");
|
||||
|
||||
#[derive(Default)]
|
||||
struct Asset {
|
||||
count: AtomicUsize,
|
||||
started: Notify,
|
||||
released: AtomicBool,
|
||||
release: Notify,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
async fn wait(&self, release: bool) {
|
||||
loop {
|
||||
let notified = if release {
|
||||
self.release.notified()
|
||||
} else {
|
||||
self.started.notified()
|
||||
};
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
if if release {
|
||||
self.released.load(Ordering::Acquire)
|
||||
} else {
|
||||
self.count.load(Ordering::Acquire) != 0
|
||||
} {
|
||||
return;
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PreloadServer {
|
||||
origin: String,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for PreloadServer {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl PreloadServer {
|
||||
async fn spawn() -> Result<Self> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let origin = format!("http://{}", listener.local_addr()?);
|
||||
let task = tokio::spawn(async move {
|
||||
let assets = Arc::new(Mutex::new(HashMap::<String, Arc<Asset>>::new()));
|
||||
let mut connections = JoinSet::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
accepted = listener.accept() => {
|
||||
let Ok((mut stream, _)) = accepted else { break };
|
||||
let assets = assets.clone();
|
||||
connections.spawn(async move {
|
||||
let mut request = Vec::new();
|
||||
while !request.ends_with(b"\r\n\r\n") && request.len() < 16384 {
|
||||
let Ok(byte) = stream.read_u8().await else { return };
|
||||
request.push(byte);
|
||||
}
|
||||
let request = String::from_utf8_lossy(&request);
|
||||
let path = request.split_whitespace().nth(1).unwrap_or("/");
|
||||
let url = url::Url::parse(&format!("http://test{path}")).unwrap();
|
||||
let query: HashMap<_,_> = url.query_pairs().into_owned().collect();
|
||||
let token = query.get("token").cloned().unwrap_or_default();
|
||||
let asset = assets.lock().entry(token).or_default().clone();
|
||||
let mut cache = "no-store".to_owned();
|
||||
let (mime, body) = match url.path() {
|
||||
"/probe-worker.js" => ("text/javascript", include_str!("../fixtures/preload-consumption-worker.js").to_owned()),
|
||||
"/probe-asset" => {
|
||||
asset.count.fetch_add(1, Ordering::AcqRel);
|
||||
asset.started.notify_waiters();
|
||||
asset.wait(true).await;
|
||||
cache = query.get("cache").cloned().unwrap_or(cache);
|
||||
if query.get("as").is_some_and(|kind| kind == "image") {
|
||||
("image/svg+xml", "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1'/></svg>".to_owned())
|
||||
} else { ("text/javascript", SCRIPT.to_owned()) }
|
||||
}
|
||||
"/probe-started" | "/probe-release" | "/probe-stats" => {
|
||||
if url.path() == "/probe-started" { asset.wait(false).await; }
|
||||
if url.path() == "/probe-release" {
|
||||
asset.released.store(true, Ordering::Release);
|
||||
asset.release.notify_waiters();
|
||||
}
|
||||
("application/json", serde_json::json!({"count":asset.count.load(Ordering::Acquire)}).to_string())
|
||||
}
|
||||
_ => ("text/html", "<!doctype html><body>preload consumption".to_owned()),
|
||||
};
|
||||
let origin = request.lines().find_map(|line| line.split_once(':').filter(|(name,_)| name.eq_ignore_ascii_case("Origin")).map(|(_,value)| value.trim())).unwrap_or("*");
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: {mime}\r\nCache-Control: {cache}\r\nAccess-Control-Allow-Origin: {origin}\r\nAccess-Control-Allow-Credentials: true\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
|
||||
let _ = stream.write_all(response.as_bytes()).await;
|
||||
});
|
||||
}
|
||||
_ = connections.join_next(), if !connections.is_empty() => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Self { origin, task })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn document_preloads_supply_pending_and_completed_consumers_including_failed_integrity()
|
||||
-> Result<()> {
|
||||
let server = PreloadServer::spawn().await?;
|
||||
let mut config = AppConfig::default();
|
||||
config.set_optional_resource_fetch_mask(moli_page_types::OptionalResourceFetchMask::ALL);
|
||||
let browser = Browser::new(config)?;
|
||||
let mut page = browser.fetch(&server.origin).await?;
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
page.evaluate_runtime_expression_with_await_async(
|
||||
&format!("({PROBE})().then(JSON.stringify)"),
|
||||
true,
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
let value: serde_json::Value = serde_json::from_str(
|
||||
result["value"]
|
||||
.as_str()
|
||||
.context("preload consumption results")?,
|
||||
)?;
|
||||
assert_eq!(value["basic"]["rows"].as_array().unwrap().len(), 24);
|
||||
assert_eq!(value["integrity"]["rows"].as_array().unwrap().len(), 32);
|
||||
assert_eq!(value["basic"]["failures"], serde_json::json!([]));
|
||||
assert_eq!(value["integrity"]["failures"], serde_json::json!([]));
|
||||
assert_eq!(value["basic"]["executions"], 12);
|
||||
assert_eq!(value["oneShot"], serde_json::json!([1, 2, 2, 3]));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn document_preload_consumption_preserves_service_worker_filters_without_redispatch()
|
||||
-> Result<()> {
|
||||
let server = PreloadServer::spawn().await?;
|
||||
let mut config = AppConfig::default();
|
||||
config.set_optional_resource_fetch_mask(moli_page_types::OptionalResourceFetchMask::ALL);
|
||||
let browser = Browser::new(config)?;
|
||||
let mut page = browser.fetch(&server.origin).await?;
|
||||
let probe = include_str!("../fixtures/preload-consumption-sw.js");
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
page.evaluate_runtime_expression_with_await_async(
|
||||
&format!("({probe})().then(JSON.stringify)"),
|
||||
true,
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
let value: serde_json::Value = serde_json::from_str(
|
||||
result["value"]
|
||||
.as_str()
|
||||
.context("Service Worker consumption results")?,
|
||||
)?;
|
||||
assert_eq!(value["rows"].as_array().unwrap().len(), 16);
|
||||
assert_eq!(value["failures"], serde_json::json!([]));
|
||||
Ok(())
|
||||
}
|
||||
@@ -538,6 +538,7 @@ impl StreamingResponseCollector {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
network_request_extra_info: self.network_request_extra_info.clone(),
|
||||
}));
|
||||
@@ -942,6 +943,7 @@ impl RawStreamingResponseCollector {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
network_request_extra_info: self.network_request_extra_info.clone(),
|
||||
}));
|
||||
|
||||
@@ -74,6 +74,7 @@ pub struct StreamingHtmlResponseStart {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
pub network_request_extra_info: Option<NetworkRequestExtraInfo>,
|
||||
}
|
||||
@@ -91,6 +92,7 @@ impl StreamingHtmlResponseStart {
|
||||
redirect_chain: self.redirect_chain,
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ pub use request::{
|
||||
};
|
||||
pub use request_policy::{is_bad_port, should_request_be_blocked_due_to_bad_port};
|
||||
pub use response::{
|
||||
NegotiatedHttpVersion, NetworkRequestExtraInfo, NetworkResponseExtraInfo, RawResponse,
|
||||
RedirectInfo, RedirectSource, Response, ResponseBody, ResponseCacheState, ResponseHead,
|
||||
FetchResponseFilter, NegotiatedHttpVersion, NetworkRequestExtraInfo, NetworkResponseExtraInfo,
|
||||
RawResponse, RedirectInfo, RedirectSource, Response, ResponseBody, ResponseCacheState,
|
||||
ResponseHead, ResponsePreloadState,
|
||||
};
|
||||
pub use runtime::PendingStreamingRawResponse;
|
||||
pub use runtime::{
|
||||
|
||||
@@ -16,6 +16,59 @@ pub enum ResponseCacheState {
|
||||
Validated,
|
||||
}
|
||||
|
||||
/// The actual Fetch response filter, including Service Worker responses whose
|
||||
/// type cannot be reconstructed from the request or final URL.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum FetchResponseFilter {
|
||||
Basic,
|
||||
Cors(Vec<String>),
|
||||
Opaque,
|
||||
OpaqueRedirect,
|
||||
}
|
||||
|
||||
impl FetchResponseFilter {
|
||||
pub fn is_readable(&self) -> bool {
|
||||
matches!(self, Self::Basic | Self::Cors(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// A consumed preload retains its response filter and reports Resource Timing
|
||||
/// only for the producer. This is independent of HTTP cache state: a no-store
|
||||
/// response can also be preloaded.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ResponsePreloadState {
|
||||
#[default]
|
||||
None,
|
||||
Consumed {
|
||||
filter: Option<FetchResponseFilter>,
|
||||
from_service_worker: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResponsePreloadState {
|
||||
pub fn is_consumed(&self) -> bool {
|
||||
matches!(self, Self::Consumed { .. })
|
||||
}
|
||||
|
||||
pub fn from_service_worker(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Consumed {
|
||||
from_service_worker: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn response_filter(&self) -> Option<&FetchResponseFilter> {
|
||||
match self {
|
||||
Self::Consumed { filter, .. } => filter.as_ref(),
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NegotiatedHttpVersion {
|
||||
Http09,
|
||||
@@ -64,6 +117,7 @@ pub struct ResponseHead {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
}
|
||||
|
||||
@@ -341,6 +395,7 @@ pub struct Response {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
network_request_extra_info: Option<NetworkRequestExtraInfo>,
|
||||
}
|
||||
@@ -362,6 +417,7 @@ impl Clone for Response {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
network_request_extra_info: self.network_request_extra_info.clone(),
|
||||
}
|
||||
@@ -417,6 +473,7 @@ impl Response {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
@@ -456,6 +513,7 @@ impl Response {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
})
|
||||
@@ -475,6 +533,7 @@ impl Response {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
})
|
||||
@@ -492,6 +551,7 @@ impl Response {
|
||||
redirect_chain: self.redirect_chain,
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
};
|
||||
let (body, body_bytes) = self
|
||||
@@ -530,6 +590,7 @@ impl Response {
|
||||
redirect_chain: self.redirect_chain,
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
};
|
||||
(head, self.body)
|
||||
@@ -557,6 +618,7 @@ pub struct RawResponse {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
network_request_extra_info: Option<NetworkRequestExtraInfo>,
|
||||
}
|
||||
@@ -578,6 +640,7 @@ impl Clone for RawResponse {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
network_request_extra_info: self.network_request_extra_info.clone(),
|
||||
}
|
||||
@@ -627,6 +690,7 @@ impl RawResponse {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
@@ -652,6 +716,7 @@ impl RawResponse {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
})
|
||||
@@ -671,6 +736,7 @@ impl RawResponse {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
})
|
||||
@@ -688,6 +754,7 @@ impl RawResponse {
|
||||
redirect_chain: self.redirect_chain,
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
};
|
||||
(head, self.body)
|
||||
|
||||
@@ -2345,6 +2345,7 @@ impl RuntimeOwner {
|
||||
redirect_chain: job.request.redirect_chain,
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version,
|
||||
network_request_extra_info: request_extra_info,
|
||||
}));
|
||||
@@ -2922,6 +2923,7 @@ fn complete_streaming_html_job(job: StreamingRuntimeJob, response: Response) {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info,
|
||||
}));
|
||||
@@ -2977,6 +2979,7 @@ fn complete_cached_streaming_html_job(
|
||||
redirect_chain,
|
||||
from_cache: true,
|
||||
cache_state,
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
network_request_extra_info: None,
|
||||
}));
|
||||
@@ -3101,6 +3104,7 @@ fn complete_cached_streaming_raw_job(
|
||||
redirect_chain,
|
||||
from_cache: true,
|
||||
cache_state,
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
network_request_extra_info: None,
|
||||
}));
|
||||
@@ -3544,6 +3548,7 @@ fn proxy_connect_response_start(
|
||||
redirect_chain: redirect_chain.to_vec(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
network_request_extra_info: None,
|
||||
}
|
||||
@@ -3648,6 +3653,7 @@ fn collect_buffered_response(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version,
|
||||
},
|
||||
body,
|
||||
@@ -4309,6 +4315,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"<!doctype html><html><body>cached</body></html>".to_owned(),
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct StreamingHtmlResponse {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
network_request_extra_info: Option<NetworkRequestExtraInfo>,
|
||||
body_chunks: mpsc::UnboundedReceiver<String>,
|
||||
@@ -60,6 +61,7 @@ impl StreamingHtmlResponse {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_chunks,
|
||||
@@ -85,6 +87,7 @@ impl StreamingHtmlResponse {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
body_chunks,
|
||||
@@ -105,6 +108,7 @@ impl StreamingHtmlResponse {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
@@ -179,6 +183,7 @@ pub struct StreamingRawResponse {
|
||||
pub redirect_chain: Vec<RedirectInfo>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: crate::ResponseCacheState,
|
||||
pub preload_state: crate::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
network_request_extra_info: Option<NetworkRequestExtraInfo>,
|
||||
body_chunks: mpsc::UnboundedReceiver<Vec<u8>>,
|
||||
@@ -211,6 +216,7 @@ impl StreamingRawResponse {
|
||||
redirect_chain,
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
network_request_extra_info: None,
|
||||
body_chunks,
|
||||
@@ -237,6 +243,7 @@ impl StreamingRawResponse {
|
||||
redirect_chain: head.redirect_chain,
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_extra_info: None,
|
||||
body_chunks,
|
||||
@@ -329,6 +336,7 @@ impl StreamingRawResponse {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
@@ -396,6 +404,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ fn sample_response_head() -> ResponseHead {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ pub struct NavigationResponse {
|
||||
pub redirect_chain: Vec<NavigationRedirect>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: moli_fetch::ResponseCacheState,
|
||||
pub preload_state: moli_fetch::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<NegotiatedHttpVersion>,
|
||||
network_request_headers: Option<Vec<(String, String)>>,
|
||||
}
|
||||
@@ -348,6 +349,7 @@ impl Clone for NavigationResponse {
|
||||
redirect_chain: self.redirect_chain.clone(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
network_request_headers: self.network_request_headers.clone(),
|
||||
}
|
||||
@@ -410,6 +412,7 @@ impl NavigationResponse {
|
||||
.collect(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
@@ -452,6 +455,7 @@ impl NavigationResponse {
|
||||
redirect_chain: head.redirect_chain.into_iter().map(Into::into).collect(),
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_headers: None,
|
||||
}
|
||||
@@ -479,6 +483,7 @@ impl NavigationResponse {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body,
|
||||
@@ -512,6 +517,7 @@ impl NavigationResponse {
|
||||
redirect_chain: self.redirect_chain.into_iter().map(Into::into).collect(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
};
|
||||
let (body, body_bytes) = self
|
||||
@@ -550,6 +556,7 @@ impl NavigationResponse {
|
||||
redirect_chain: self.redirect_chain.into_iter().map(Into::into).collect(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
};
|
||||
(head, self.body)
|
||||
@@ -2439,6 +2446,7 @@ pub struct PendingSubresourceResponseInfo {
|
||||
pub response_body: SubresourceResponseBody,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: moli_fetch::ResponseCacheState,
|
||||
pub preload_state: moli_fetch::ResponsePreloadState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -2459,6 +2467,7 @@ pub struct PendingSubresourceAuthInfo {
|
||||
pub response_body: SubresourceResponseBody,
|
||||
pub response_from_cache: bool,
|
||||
pub response_cache_state: moli_fetch::ResponseCacheState,
|
||||
pub response_preload_state: moli_fetch::ResponsePreloadState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -3321,6 +3330,8 @@ pub struct ChildFrameDocumentNetworkSnapshot {
|
||||
pub from_cache: bool,
|
||||
#[serde(default)]
|
||||
pub cache_state: moli_fetch::ResponseCacheState,
|
||||
#[serde(skip)]
|
||||
pub preload_state: moli_fetch::ResponsePreloadState,
|
||||
}
|
||||
|
||||
/// A completed child main-resource request whose Network facts remain
|
||||
@@ -4045,6 +4056,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"hello".to_owned(),
|
||||
@@ -4073,6 +4085,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"é".to_owned(),
|
||||
@@ -4112,6 +4125,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
String::new(),
|
||||
|
||||
@@ -274,6 +274,7 @@ impl PendingFetchAuthNavigation {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
b"auth required".to_vec(),
|
||||
|
||||
@@ -409,6 +409,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body.to_vec(),
|
||||
|
||||
@@ -160,6 +160,7 @@ async fn prepare_browser_owned_error_page_navigation_with_engine_async(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
prepare_captured_document_response_with_engine_async(
|
||||
@@ -1217,6 +1218,7 @@ fn decoded_data_url_navigation_response(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
decoded.body,
|
||||
@@ -1314,6 +1316,7 @@ async fn load_inline_html_navigation_with_engine_async(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
prepare_navigation_from_captured_raw_response_with_engine_async(
|
||||
@@ -3007,6 +3010,7 @@ impl CdpConnection {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
self.build_navigation_from_captured_raw_response_with_load_inputs_async(
|
||||
|
||||
@@ -630,6 +630,7 @@ fn pause_data_url_response_stage_navigation_into_buffer(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body,
|
||||
|
||||
@@ -593,6 +593,7 @@ mod tests {
|
||||
response_body: SubresourceResponseBody::from_bytes(b"prepared".to_vec()),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
});
|
||||
let action = crate::domains::activity::PreparedSubresourceContinueAction::capture_for_test(
|
||||
&mut conn,
|
||||
@@ -671,6 +672,7 @@ mod tests {
|
||||
response_body: SubresourceResponseBody::from_bytes(b"old".to_vec()),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
});
|
||||
let old_action =
|
||||
crate::domains::activity::PreparedSubresourceContinueAction::capture_for_test(
|
||||
|
||||
@@ -690,6 +690,7 @@ async fn clear_browser_cache_keeps_pending_response_navigation_transfer() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
b"body".to_vec(),
|
||||
|
||||
@@ -1644,6 +1644,7 @@ async fn child_frame_activity_emits_document_network_events_from_prepared_load()
|
||||
response_body: Some(SubresourceResponseBody::from_bytes(vec![0x00, 0xff, b'a'])),
|
||||
from_cache: true,
|
||||
cache_state: moli_fetch::ResponseCacheState::Local,
|
||||
preload_state: Default::default(),
|
||||
}),
|
||||
}],
|
||||
"https://example.test".to_owned(),
|
||||
@@ -1776,6 +1777,7 @@ async fn stale_child_document_response_emits_network_without_navigation_or_lifec
|
||||
)),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
},
|
||||
}],
|
||||
Vec::new(),
|
||||
@@ -1878,6 +1880,7 @@ async fn child_document_network_without_body_records_known_no_data() {
|
||||
response_body: None,
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
};
|
||||
let mut background_events = Vec::new();
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ fn record_performance_load_event_for_window(
|
||||
}
|
||||
|
||||
pub(crate) struct ResourcePerformanceEntry {
|
||||
preload_state: moli_fetch::ResponsePreloadState,
|
||||
name: String,
|
||||
initiator_type: String,
|
||||
start_unix_millis: Option<f64>,
|
||||
@@ -299,6 +300,7 @@ impl ResourcePerformanceEntry {
|
||||
&response.headers,
|
||||
body_size as f64,
|
||||
response.cache_state,
|
||||
response.preload_state.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,6 +319,7 @@ impl ResourcePerformanceEntry {
|
||||
&response.headers,
|
||||
body_size as f64,
|
||||
response.cache_state,
|
||||
response.preload_state.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -334,6 +337,7 @@ impl ResourcePerformanceEntry {
|
||||
&response.headers,
|
||||
response.body_bytes().len() as f64,
|
||||
response.cache_state,
|
||||
response.preload_state.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -345,6 +349,7 @@ impl ResourcePerformanceEntry {
|
||||
response_headers: &[(String, Vec<u8>)],
|
||||
body_size: f64,
|
||||
cache_state: moli_fetch::ResponseCacheState,
|
||||
preload_state: moli_fetch::ResponsePreloadState,
|
||||
) -> Self {
|
||||
// Resource Timing deliberately uses a fixed overhead rather than
|
||||
// exposing response-header sizes, which could reveal cookies.
|
||||
@@ -357,6 +362,7 @@ impl ResourcePerformanceEntry {
|
||||
.and_then(|value| moli_web_mime::mime_essence(&value))
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
preload_state,
|
||||
name: name.into(),
|
||||
initiator_type: initiator_type.to_owned(),
|
||||
start_unix_millis,
|
||||
@@ -385,6 +391,7 @@ impl ResourcePerformanceEntry {
|
||||
&network.response_headers,
|
||||
network.encoded_data_length as f64,
|
||||
network.cache_state,
|
||||
network.preload_state.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -402,6 +409,7 @@ impl ResourcePerformanceEntry {
|
||||
start_unix_millis: Option<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
preload_state: Default::default(),
|
||||
name: name.into(),
|
||||
initiator_type: initiator_type.to_owned(),
|
||||
start_unix_millis,
|
||||
@@ -419,6 +427,9 @@ pub(crate) fn record_resource_performance_entry(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
entry: ResourcePerformanceEntry,
|
||||
) {
|
||||
if entry.preload_state.is_consumed() {
|
||||
return;
|
||||
}
|
||||
if !url::Url::parse(&entry.name).is_ok_and(|url| matches!(url.scheme(), "http" | "https")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ pub(in crate::context_bootstrap::performance_runtime) fn take_pending_resource_e
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())?;
|
||||
Some(ResourcePerformanceEntry {
|
||||
preload_state: Default::default(),
|
||||
name: array_string(scope, record, 0)?,
|
||||
initiator_type: array_string(scope, record, 1)?,
|
||||
start_unix_millis: record
|
||||
|
||||
@@ -1109,6 +1109,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: true,
|
||||
cache_state: moli_fetch::ResponseCacheState::Local,
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"export default 1;".to_owned(),
|
||||
|
||||
@@ -889,6 +889,7 @@ fn child_document_load_outcome_from_response(
|
||||
response_body: Some(response_body),
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
}),
|
||||
},
|
||||
)))
|
||||
@@ -958,6 +959,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(body_bytes.clone()),
|
||||
|
||||
@@ -3086,6 +3086,7 @@ impl JsContextHost {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: info.from_cache,
|
||||
cache_state: info.cache_state,
|
||||
preload_state: info.preload_state.clone(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
)
|
||||
@@ -3132,6 +3133,7 @@ impl JsContextHost {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: info.response_from_cache,
|
||||
cache_state: info.response_cache_state,
|
||||
preload_state: info.response_preload_state.clone(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1773,6 +1773,44 @@ impl JsContextHost {
|
||||
&self,
|
||||
dispatch: ServiceWorkerFetchDispatch,
|
||||
) -> bool {
|
||||
use crate::service_worker_runtime::ServiceWorkerRequestDestination as Destination;
|
||||
use moli_fetch::BrowserRequestMetadata as Metadata;
|
||||
let metadata = match dispatch.request.destination {
|
||||
Destination::Script => Some(Metadata::Script),
|
||||
Destination::Style => Some(Metadata::Style),
|
||||
Destination::Image => Some(Metadata::Image),
|
||||
Destination::Font => Some(Metadata::Font),
|
||||
Destination::Track => Some(Metadata::TextTrack),
|
||||
Destination::Empty => Some(Metadata::Fetch),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(metadata) = metadata {
|
||||
let request = moli_fetch::Request::new_browser(
|
||||
&dispatch.request.method,
|
||||
dispatch.request.url.clone(),
|
||||
dispatch.request.body.clone(),
|
||||
moli_fetch::RequestHeaders::from_byte_strings(&dispatch.request.headers)
|
||||
.expect("service worker request headers are ByteStrings"),
|
||||
dispatch.network_context.request_origin.clone(),
|
||||
)
|
||||
.with_initiator_url(&dispatch.network_context.document_url)
|
||||
.with_request_mode(dispatch.request.request_mode)
|
||||
.with_credentials_mode(dispatch.request.credentials_mode)
|
||||
.with_browser_request_metadata(metadata)
|
||||
.with_subresource_request_metadata(
|
||||
moli_fetch::SubresourceRequestMetadata {
|
||||
integrity: Some(dispatch.request.metadata.integrity.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if let Some(preload) = dispatch.request_client.consume_document_preload(&request) {
|
||||
dispatch.resource_task_runner.spawn(async move {
|
||||
let completion = preload.into_completion(dispatch.internal_id, request).await;
|
||||
let _ = dispatch.completion_tx.send_async_subresource(completion);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.browser_context_runtime
|
||||
.dispatch_service_worker_fetch(dispatch)
|
||||
}
|
||||
|
||||
@@ -307,6 +307,7 @@ impl JsContextHost {
|
||||
response_body: SubresourceResponseBody::from_bytes(Vec::new()),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
}),
|
||||
);
|
||||
true
|
||||
|
||||
@@ -837,6 +837,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body.to_owned(),
|
||||
@@ -856,6 +857,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body.to_vec(),
|
||||
@@ -1061,6 +1063,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"x".repeat(64),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use moli_cookie_jar::BrowserCookieFacadeContext;
|
||||
@@ -51,6 +51,8 @@ struct DocumentResourceLoaderAuthority {
|
||||
id: u64,
|
||||
lifecycle: Mutex<DocumentResourceLoaderLifecycle>,
|
||||
loads: ResourceLoadRegistry,
|
||||
preloads: crate::network::preloads::DocumentPreloads,
|
||||
owns_preloads: AtomicBool,
|
||||
}
|
||||
|
||||
struct DocumentResourceLoaderLifecycle {
|
||||
@@ -64,6 +66,9 @@ impl Drop for DocumentResourceLoaderAuthority {
|
||||
// boundary. This final guard covers construction failures and runtime
|
||||
// teardown paths that drop the authority before publishing it.
|
||||
self.loads.begin_detach();
|
||||
if self.owns_preloads.load(Ordering::Acquire) {
|
||||
self.preloads.retire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +148,12 @@ impl DocumentResourceLoader {
|
||||
let loads = ResourceLoadRegistry::new(task_runner);
|
||||
// The Fetch client origin is independent of a script's referrer/base
|
||||
// URL, including cross-origin dependencies and inherited/sandboxed Documents.
|
||||
let request_client = request_client.with_script_request_origin(
|
||||
moli_url::WebOrigin::from_ascii_serialization(context.origin()),
|
||||
);
|
||||
let preloads = crate::network::preloads::DocumentPreloads::default();
|
||||
let request_client = request_client
|
||||
.with_script_request_origin(moli_url::WebOrigin::from_ascii_serialization(
|
||||
context.origin(),
|
||||
))
|
||||
.with_document_preloads(preloads.clone());
|
||||
Self {
|
||||
request_client,
|
||||
csp_reports: Default::default(),
|
||||
@@ -158,6 +166,8 @@ impl DocumentResourceLoader {
|
||||
context,
|
||||
}),
|
||||
loads,
|
||||
preloads,
|
||||
owns_preloads: AtomicBool::new(true),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -204,6 +214,18 @@ impl DocumentResourceLoader {
|
||||
// document.open() rotates Moli's load owner, but keeps the Document's
|
||||
// CSP. In-flight requests and new requests must share its report history.
|
||||
replacement.csp_reports = previous.csp_reports.clone();
|
||||
// document.open() replaces the load owner, not the Document. Transfer
|
||||
// retirement authority along with its one-shot preload map.
|
||||
previous
|
||||
.authority
|
||||
.owns_preloads
|
||||
.store(false, Ordering::Release);
|
||||
Arc::get_mut(&mut replacement.authority)
|
||||
.expect("new Document resource authority is unshared")
|
||||
.preloads = previous.authority.preloads.clone();
|
||||
replacement.request_client = replacement
|
||||
.request_client
|
||||
.with_document_preloads(previous.authority.preloads.clone());
|
||||
replacement
|
||||
}
|
||||
|
||||
@@ -238,11 +260,11 @@ impl DocumentResourceLoader {
|
||||
if let Some(browser_site_context) = self.request_client.shared_browser_site_context() {
|
||||
request_client = request_client.with_shared_browser_site_context(browser_site_context);
|
||||
}
|
||||
request_client = request_client.with_script_request_origin(
|
||||
moli_url::WebOrigin::from_ascii_serialization(
|
||||
request_client = request_client
|
||||
.with_script_request_origin(moli_url::WebOrigin::from_ascii_serialization(
|
||||
self.authority.lifecycle.lock().context.origin(),
|
||||
),
|
||||
);
|
||||
))
|
||||
.with_document_preloads(self.authority.preloads.clone());
|
||||
Self {
|
||||
request_client,
|
||||
authority: Arc::clone(&self.authority),
|
||||
@@ -258,6 +280,9 @@ impl DocumentResourceLoader {
|
||||
lifecycle.state = DocumentResourceLoaderState::Detaching;
|
||||
drop(lifecycle);
|
||||
self.authority.loads.begin_detach();
|
||||
if self.authority.owns_preloads.load(Ordering::Acquire) {
|
||||
self.authority.preloads.retire();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -528,3 +528,53 @@ async fn dropping_unpublished_worker_authority_cancels_ordinary_loads() {
|
||||
assert!(cancel.is_cancelled());
|
||||
assert!(load.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_preloads_survive_transport_replacement_and_open_but_not_new_documents() {
|
||||
let transport = ResourceRequestClient::new(&FetchConfig::default()).unwrap();
|
||||
let first = document_loader(transport.clone(), 1, "https://example.test/page");
|
||||
let request = moli_fetch::Request::new("GET", "https://example.test/asset", None, vec![])
|
||||
.unwrap()
|
||||
.with_page_network_policy()
|
||||
.with_request_mode(moli_fetch::RequestMode::NoCors)
|
||||
.with_browser_request_metadata(moli_fetch::BrowserRequestMetadata::Script);
|
||||
let producer = first
|
||||
.request_client()
|
||||
.register_document_preload(&request)
|
||||
.unwrap();
|
||||
// A preload lookup is independent of whether the network transport can
|
||||
// currently make a request, both on a miss and when consuming a response.
|
||||
transport.set_network_offline(true);
|
||||
let other = first.fork_for_document(context(2, "https://example.test/page"));
|
||||
assert!(
|
||||
other
|
||||
.request_client()
|
||||
.consume_document_preload(&request)
|
||||
.is_none()
|
||||
);
|
||||
let replaced = first.with_replacement_transport(transport.handle());
|
||||
let opened = DocumentResourceLoader::for_document_open(
|
||||
context(3, "https://example.test/page"),
|
||||
super::document::DocumentResourceAuthoritySource::Inherited(replaced.clone()),
|
||||
&replaced,
|
||||
);
|
||||
first.begin_detach();
|
||||
let captured = opened.request_client().frozen_request_client();
|
||||
let consumer = captured.consume_document_preload(&request).unwrap();
|
||||
producer.complete(Err("retained preload failure".to_owned()));
|
||||
assert_eq!(
|
||||
consumer.response().await.unwrap_err(),
|
||||
"retained preload failure"
|
||||
);
|
||||
let producer = opened
|
||||
.request_client()
|
||||
.register_document_preload(&request)
|
||||
.unwrap();
|
||||
let consumer = captured.consume_document_preload(&request).unwrap();
|
||||
opened.begin_detach();
|
||||
assert_eq!(
|
||||
consumer.response().await.unwrap_err(),
|
||||
"preload Document retired"
|
||||
);
|
||||
drop(producer);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ impl RendererPreparedNetworkResourceLoad {
|
||||
redirect_chain: head.redirect_chain.into_iter().map(Into::into).collect(),
|
||||
from_cache: head.from_cache,
|
||||
cache_state: head.cache_state,
|
||||
preload_state: head.preload_state.clone(),
|
||||
negotiated_http_version: head.negotiated_http_version,
|
||||
network_request_headers,
|
||||
},
|
||||
@@ -92,6 +93,7 @@ pub struct RendererNetworkResourceLoadResponse {
|
||||
pub redirect_chain: Vec<NavigationRedirect>,
|
||||
pub from_cache: bool,
|
||||
pub cache_state: moli_fetch::ResponseCacheState,
|
||||
pub preload_state: moli_fetch::ResponsePreloadState,
|
||||
pub negotiated_http_version: Option<moli_fetch::NegotiatedHttpVersion>,
|
||||
pub network_request_headers: Option<Vec<(String, String)>>,
|
||||
}
|
||||
@@ -114,6 +116,7 @@ impl RendererNetworkResourceLoadResponse {
|
||||
.collect(),
|
||||
from_cache: self.from_cache,
|
||||
cache_state: self.cache_state,
|
||||
preload_state: self.preload_state.clone(),
|
||||
negotiated_http_version: self.negotiated_http_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ mod devtools_resource_load;
|
||||
pub(crate) mod loads;
|
||||
pub mod navigation;
|
||||
mod policy;
|
||||
pub(crate) mod preloads;
|
||||
mod request_client;
|
||||
mod task_runner;
|
||||
|
||||
@@ -29,6 +30,7 @@ pub use devtools_resource_load::{
|
||||
RendererNetworkResourceLoadResponse, RendererPreparedNetworkResourceLoad,
|
||||
};
|
||||
pub use policy::{PageNetworkPolicy, PageNetworkPolicySnapshot};
|
||||
pub(crate) use request_client::streaming_raw_response_from_local_response;
|
||||
pub use request_client::{ResourceRequestClient, ResourceRequestClientOwner};
|
||||
pub use task_runner::RendererResourceTaskRunner;
|
||||
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use moli_fetch::{BrowserRequestMetadata, Request, RequestCredentialsMode, RequestMode};
|
||||
use parking_lot::Mutex;
|
||||
use tokio::sync::Notify;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
protocol_types::NavigationResponse,
|
||||
subresource_integrity::integrity_metadata_allows_preload_consumption,
|
||||
types::AsyncSubresourceFetchResponseFilter,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DocumentPreloadResponse {
|
||||
pub(crate) response: NavigationResponse,
|
||||
pub(crate) from_service_worker: bool,
|
||||
pub(crate) response_filter: Option<AsyncSubresourceFetchResponseFilter>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ConsumedPreloadError(pub(crate) String);
|
||||
|
||||
impl std::fmt::Display for ConsumedPreloadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConsumedPreloadError {}
|
||||
|
||||
type PreloadResult = Result<DocumentPreloadResponse, String>;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
struct PreloadKey {
|
||||
url: Url,
|
||||
destination: &'static str,
|
||||
mode: RequestMode,
|
||||
credentials: RequestCredentialsMode,
|
||||
}
|
||||
|
||||
impl PreloadKey {
|
||||
fn from_request(request: &Request) -> Option<Self> {
|
||||
if request.method != "GET"
|
||||
|| request.body.is_some()
|
||||
|| !matches!(
|
||||
request.request_mode,
|
||||
RequestMode::NoCors | RequestMode::Cors | RequestMode::SameOrigin
|
||||
)
|
||||
|| !matches!(request.url.scheme(), "http" | "https")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let destination = match request.browser_request_metadata()? {
|
||||
BrowserRequestMetadata::Script => "script",
|
||||
BrowserRequestMetadata::Style | BrowserRequestMetadata::StyleModule => "style",
|
||||
BrowserRequestMetadata::Image => "image",
|
||||
BrowserRequestMetadata::Font => "font",
|
||||
BrowserRequestMetadata::TextTrack => "track",
|
||||
BrowserRequestMetadata::Fetch | BrowserRequestMetadata::Xhr => "",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self {
|
||||
url: request.url.clone(),
|
||||
destination,
|
||||
mode: request.request_mode,
|
||||
credentials: request.credentials_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PreloadEntry {
|
||||
integrity: String,
|
||||
result: Mutex<Option<PreloadResult>>,
|
||||
available: Notify,
|
||||
}
|
||||
|
||||
impl PreloadEntry {
|
||||
fn complete(&self, result: PreloadResult) {
|
||||
let mut current = self.result.lock();
|
||||
if current.is_some() {
|
||||
return;
|
||||
}
|
||||
*current = Some(result);
|
||||
drop(current);
|
||||
self.available.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PreloadMap {
|
||||
retired: bool,
|
||||
entries: HashMap<PreloadKey, Arc<PreloadEntry>>,
|
||||
// Includes consumed entries, so retiring the Document also wakes consumers
|
||||
// waiting for a response that has not yet reached the loader.
|
||||
pending: Vec<Weak<PreloadEntry>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct DocumentPreloads(Arc<Mutex<PreloadMap>>);
|
||||
|
||||
impl DocumentPreloads {
|
||||
pub(crate) fn register(&self, request: &Request) -> Option<DocumentPreloadProducer> {
|
||||
let key = PreloadKey::from_request(request)?;
|
||||
let mut map = self.0.lock();
|
||||
if map.retired {
|
||||
return None;
|
||||
}
|
||||
let entry = Arc::new(PreloadEntry {
|
||||
integrity: request
|
||||
.subresource_request_metadata()
|
||||
.and_then(|metadata| metadata.integrity.as_deref())
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
result: Mutex::new(None),
|
||||
available: Notify::new(),
|
||||
});
|
||||
map.pending.retain(|entry| entry.strong_count() != 0);
|
||||
map.pending.push(Arc::downgrade(&entry));
|
||||
map.entries.insert(key, entry.clone());
|
||||
Some(DocumentPreloadProducer { entry })
|
||||
}
|
||||
|
||||
pub(crate) fn consume(&self, request: &Request) -> Option<DocumentPreloadConsumer> {
|
||||
if request.priority_hints.link_preload {
|
||||
return None;
|
||||
}
|
||||
// Fetch/XHR author headers make the request ineligible for preload
|
||||
// consumption. DOM-initiated requests do not set the unsafe-request flag.
|
||||
if matches!(
|
||||
request.browser_request_metadata(),
|
||||
Some(BrowserRequestMetadata::Fetch | BrowserRequestMetadata::Xhr)
|
||||
) && !request.request_headers.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let key = PreloadKey::from_request(request)?;
|
||||
let mut map = self.0.lock();
|
||||
if map.retired {
|
||||
return None;
|
||||
}
|
||||
let entry = map.entries.get(&key)?;
|
||||
if !integrity_metadata_allows_preload_consumption(
|
||||
&entry.integrity,
|
||||
request
|
||||
.subresource_request_metadata()
|
||||
.and_then(|metadata| metadata.integrity.as_deref()),
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
Some(DocumentPreloadConsumer {
|
||||
entry: map.entries.remove(&key)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn retire(&self) {
|
||||
let pending = {
|
||||
let mut map = self.0.lock();
|
||||
map.retired = true;
|
||||
map.entries.clear();
|
||||
std::mem::take(&mut map.pending)
|
||||
};
|
||||
for entry in pending.into_iter().filter_map(|entry| entry.upgrade()) {
|
||||
entry.complete(Err("preload Document retired".to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentPreloadProducer {
|
||||
entry: Arc<PreloadEntry>,
|
||||
}
|
||||
|
||||
impl DocumentPreloadProducer {
|
||||
pub(crate) fn complete(self, result: PreloadResult) {
|
||||
self.entry.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DocumentPreloadProducer {
|
||||
fn drop(&mut self) {
|
||||
self.entry
|
||||
.complete(Err("preload response was cancelled".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentPreloadConsumer {
|
||||
entry: Arc<PreloadEntry>,
|
||||
}
|
||||
|
||||
impl DocumentPreloadConsumer {
|
||||
pub(crate) async fn into_completion(
|
||||
self,
|
||||
internal_id: u64,
|
||||
request: Request,
|
||||
) -> crate::types::AsyncSubresourceFetchCompletion {
|
||||
use crate::types::{AsyncSubresourceFetchCompletion, AsyncSubresourceFetchResult};
|
||||
let (result, response_filter, from_service_worker) = match self.response().await {
|
||||
Ok(preload) => (
|
||||
AsyncSubresourceFetchResult::Response(preload.response),
|
||||
preload.response_filter,
|
||||
preload.from_service_worker,
|
||||
),
|
||||
Err(error) => (
|
||||
AsyncSubresourceFetchResult::PreloadFailure(error),
|
||||
None,
|
||||
false,
|
||||
),
|
||||
};
|
||||
AsyncSubresourceFetchCompletion {
|
||||
internal_id,
|
||||
request_url: request.url,
|
||||
request_method: request.method,
|
||||
request_headers: request.request_headers,
|
||||
request_body: request
|
||||
.body
|
||||
.as_ref()
|
||||
.map(|body| String::from_utf8_lossy(body).into_owned()),
|
||||
response_status_text: None,
|
||||
skip_fetch_security_validation: from_service_worker,
|
||||
response_filter,
|
||||
network_error_text: None,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn response(self) -> PreloadResult {
|
||||
loop {
|
||||
let notified = self.entry.available.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
if let Some(result) = self.entry.result.lock().clone() {
|
||||
return result.map(|mut preload| {
|
||||
preload.response.preload_state = moli_fetch::ResponsePreloadState::Consumed {
|
||||
filter: preload.response_filter.clone(),
|
||||
from_service_worker: preload.from_service_worker,
|
||||
};
|
||||
preload
|
||||
});
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use moli_fetch::{ResponseCacheState, SubresourceRequestMetadata};
|
||||
|
||||
fn request() -> Request {
|
||||
Request::new("GET", "https://example.test/asset", None, vec![])
|
||||
.unwrap()
|
||||
.with_browser_request_metadata(BrowserRequestMetadata::Script)
|
||||
.with_request_mode(RequestMode::NoCors)
|
||||
.with_credentials_mode(RequestCredentialsMode::Include)
|
||||
}
|
||||
|
||||
fn response() -> DocumentPreloadResponse {
|
||||
DocumentPreloadResponse {
|
||||
response: NavigationResponse::from_text_body(
|
||||
request().url,
|
||||
200,
|
||||
vec![],
|
||||
"body".to_owned(),
|
||||
),
|
||||
response_filter: Some(AsyncSubresourceFetchResponseFilter::Opaque),
|
||||
from_service_worker: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_preloads_are_consumed_once_and_retain_response_provenance() {
|
||||
let preloads = DocumentPreloads::default();
|
||||
let request = request();
|
||||
let producer = preloads.register(&request).unwrap();
|
||||
let consumer = preloads.consume(&request).unwrap();
|
||||
assert!(preloads.consume(&request).is_none());
|
||||
let pending = tokio::spawn(consumer.response());
|
||||
tokio::task::yield_now().await;
|
||||
assert!(!pending.is_finished());
|
||||
producer.complete(Ok(response()));
|
||||
let actual = pending.await.unwrap().unwrap();
|
||||
assert_eq!(actual.response.body_bytes(), b"body");
|
||||
assert_eq!(actual.response.cache_state, ResponseCacheState::None);
|
||||
assert!(actual.response.preload_state.is_consumed());
|
||||
assert!(actual.response.preload_state.from_service_worker());
|
||||
assert_eq!(
|
||||
actual.response.preload_state.response_filter(),
|
||||
Some(&AsyncSubresourceFetchResponseFilter::Opaque)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integrity_failure_is_reused_and_incompatible_metadata_does_not_remove_it() {
|
||||
let preloads = DocumentPreloads::default();
|
||||
let producer_request =
|
||||
request().with_subresource_request_metadata(SubresourceRequestMetadata {
|
||||
integrity: Some("sha384-AAAA".to_owned()),
|
||||
..Default::default()
|
||||
});
|
||||
let producer = preloads.register(&producer_request).unwrap();
|
||||
producer.complete(Err("integrity mismatch".to_owned()));
|
||||
let other = request().with_subresource_request_metadata(SubresourceRequestMetadata {
|
||||
integrity: Some("sha384-BBBB".to_owned()),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(preloads.consume(&other).is_none());
|
||||
assert_eq!(
|
||||
preloads
|
||||
.consume(&request())
|
||||
.unwrap()
|
||||
.response()
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"integrity mismatch"
|
||||
);
|
||||
assert!(preloads.consume(&request()).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn incompatible_fetch_keys_and_author_headers_leave_preload_available() {
|
||||
let preloads = DocumentPreloads::default();
|
||||
let producer = preloads.register(&request()).unwrap();
|
||||
let mut post = request();
|
||||
post.method = "POST".to_owned();
|
||||
for other in [
|
||||
post,
|
||||
request().with_request_mode(RequestMode::Navigate),
|
||||
request().with_request_mode(RequestMode::Cors),
|
||||
request().with_credentials_mode(RequestCredentialsMode::Omit),
|
||||
request().with_browser_request_metadata(BrowserRequestMetadata::Image),
|
||||
] {
|
||||
assert!(preloads.consume(&other).is_none());
|
||||
}
|
||||
let mut producer_request = request();
|
||||
producer_request.priority_hints.link_preload = true;
|
||||
assert!(preloads.consume(&producer_request).is_none());
|
||||
drop(producer);
|
||||
assert!(
|
||||
preloads
|
||||
.consume(&request())
|
||||
.unwrap()
|
||||
.response()
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let fetch = request().with_browser_request_metadata(BrowserRequestMetadata::Fetch);
|
||||
let _producer = preloads.register(&fetch).unwrap();
|
||||
let mut with_header = fetch.clone();
|
||||
with_header.request_headers = moli_fetch::RequestHeaders::from_byte_strings(&[(
|
||||
"X-Test".to_owned(),
|
||||
"yes".to_owned(),
|
||||
)])
|
||||
.unwrap();
|
||||
assert!(preloads.consume(&with_header).is_none());
|
||||
assert!(preloads.consume(&fetch).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retirement_wakes_reserved_consumers_and_blocks_late_publication() {
|
||||
let preloads = DocumentPreloads::default();
|
||||
let producer = preloads.register(&request()).unwrap();
|
||||
let pending = tokio::spawn(preloads.consume(&request()).unwrap().response());
|
||||
preloads.retire();
|
||||
producer.complete(Ok(response()));
|
||||
assert_eq!(
|
||||
pending.await.unwrap().unwrap_err(),
|
||||
"preload Document retired"
|
||||
);
|
||||
assert!(preloads.register(&request()).is_none());
|
||||
assert!(preloads.consume(&request()).is_none());
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub struct ResourceRequestClient {
|
||||
page_network_policy: PageNetworkPolicy,
|
||||
browser_site_context: Option<Arc<BrowserCookieFacadeContext>>,
|
||||
script_request_origin: Option<Arc<moli_url::WebOrigin>>,
|
||||
document_preloads: Option<super::preloads::DocumentPreloads>,
|
||||
}
|
||||
|
||||
/// Thread-affine lifetime root for a standalone resource request client.
|
||||
@@ -121,6 +122,7 @@ impl ResourceRequestClient {
|
||||
page_network_policy,
|
||||
browser_site_context: None,
|
||||
script_request_origin: None,
|
||||
document_preloads: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +131,31 @@ impl ResourceRequestClient {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_document_preloads(
|
||||
mut self,
|
||||
preloads: super::preloads::DocumentPreloads,
|
||||
) -> Self {
|
||||
self.document_preloads = Some(preloads);
|
||||
self
|
||||
}
|
||||
|
||||
// Looking up a Document response must not apply the transport's offline
|
||||
// policy: a miss can still be handled by a Service Worker. The request
|
||||
// entry points retain their normal policy and response validation.
|
||||
pub(crate) fn consume_document_preload(
|
||||
&self,
|
||||
request: &Request,
|
||||
) -> Option<super::preloads::DocumentPreloadConsumer> {
|
||||
self.document_preloads.as_ref()?.consume(request)
|
||||
}
|
||||
|
||||
pub(crate) fn register_document_preload(
|
||||
&self,
|
||||
request: &Request,
|
||||
) -> Option<super::preloads::DocumentPreloadProducer> {
|
||||
self.document_preloads.as_ref()?.register(request)
|
||||
}
|
||||
|
||||
fn script_request_with_client_origin(&self, request: Request) -> Request {
|
||||
if request.request_origin().is_none()
|
||||
&& let Some(origin) = &self.script_request_origin
|
||||
@@ -174,6 +201,7 @@ impl ResourceRequestClient {
|
||||
);
|
||||
client.browser_site_context = self.browser_site_context.clone();
|
||||
client.script_request_origin = self.script_request_origin.clone();
|
||||
client.document_preloads = self.document_preloads.clone();
|
||||
client
|
||||
}
|
||||
|
||||
@@ -318,6 +346,13 @@ impl ResourceRequestClient {
|
||||
request: Request,
|
||||
) -> Result<Response> {
|
||||
let request = self.apply_network_policy(self.script_request_with_client_origin(request))?;
|
||||
if let Some(preload) = self.consume_document_preload(&request) {
|
||||
return preload
|
||||
.response()
|
||||
.await
|
||||
.map(|preload| preload.response.into())
|
||||
.map_err(|error| super::preloads::ConsumedPreloadError(error).into());
|
||||
}
|
||||
if let Some(result) = local_text_response(&request) {
|
||||
return result;
|
||||
}
|
||||
@@ -485,6 +520,21 @@ impl ResourceRequestClient {
|
||||
F: FnOnce(Result<Response>) + Send + 'static,
|
||||
{
|
||||
let request = self.apply_network_policy(self.script_request_with_client_origin(request))?;
|
||||
if let Some(preload) = self.consume_document_preload(&request) {
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
|
||||
resource_load.attach_consumer_cancel(move || {
|
||||
let _ = cancel_tx.send(());
|
||||
});
|
||||
resource_load.task_runner().spawn(async move {
|
||||
let result = tokio::select! {
|
||||
result = preload.response() => result.map(|preload| preload.response.into()).map_err(|error| super::preloads::ConsumedPreloadError(error).into()),
|
||||
_ = cancel_rx => Err(super::preloads::ConsumedPreloadError("preload consumer cancelled".to_owned()).into()),
|
||||
};
|
||||
resource_load.finish();
|
||||
callback(result);
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(result) = local_text_response(&request) {
|
||||
let task_runner = resource_load.task_runner();
|
||||
task_runner.spawn(async move {
|
||||
@@ -1225,7 +1275,9 @@ fn local_text_response(request: &Request) -> Option<Result<Response>> {
|
||||
.map(|result| result.map_err(anyhow::Error::msg))
|
||||
}
|
||||
|
||||
fn streaming_raw_response_from_local_response(response: Response) -> Result<StreamingRawResponse> {
|
||||
pub(crate) fn streaming_raw_response_from_local_response(
|
||||
response: Response,
|
||||
) -> Result<StreamingRawResponse> {
|
||||
let raw_response = response.into_materialized_raw_response();
|
||||
streaming_raw_response_from_head_and_body(raw_response.head(), raw_response.clone_body_bytes())
|
||||
}
|
||||
|
||||
@@ -289,6 +289,7 @@ async fn memory_cache_tee_drop_after_body_eof_cancels_pending_completion_and_rel
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_rx,
|
||||
@@ -1610,6 +1611,7 @@ async fn cached_raw_subresource_marks_redirect_hops_from_cache() -> Result<()> {
|
||||
}],
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
b"cached-final-body".to_vec(),
|
||||
|
||||
@@ -99,6 +99,10 @@ async fn fetch_browser_subresource_with_preflight_headers_and_observer(
|
||||
preflight_request_headers: Vec<(String, String)>,
|
||||
preflight_observer: Option<&CorsPreflightNetworkObserver>,
|
||||
) -> Result<NetworkFetchResult<Response>, String> {
|
||||
if let Some(preload) = loader.consume_document_preload(&request) {
|
||||
let response = preload.response().await?.response.into();
|
||||
return Ok(NetworkFetchResult::without_request_observation(response));
|
||||
}
|
||||
if browser_request_needs_cors_redirect_checks(&request) {
|
||||
return fetch_browser_subresource_with_cors_redirect_checks(
|
||||
loader,
|
||||
@@ -515,6 +519,13 @@ async fn fetch_browser_subresource_raw_stream_with_preflight_headers_and_observe
|
||||
preflight_request_headers: Vec<(String, String)>,
|
||||
preflight_observer: Option<&CorsPreflightNetworkObserver>,
|
||||
) -> Result<NetworkFetchResult<StreamingRawResponse>, String> {
|
||||
if let Some(preload) = loader.consume_document_preload(&request) {
|
||||
let response = crate::network::streaming_raw_response_from_local_response(
|
||||
preload.response().await?.response.into(),
|
||||
)
|
||||
.map_err(format_network_error)?;
|
||||
return Ok(NetworkFetchResult::without_request_observation(response));
|
||||
}
|
||||
if browser_request_needs_cors_redirect_checks(&request) {
|
||||
return fetch_browser_subresource_raw_stream_with_cors_redirect_checks(
|
||||
loader,
|
||||
@@ -680,6 +691,11 @@ pub(crate) fn spawn_async_subresource_fetch(
|
||||
.then(|| loader.parkable_image_manager(&task_runner));
|
||||
let request = observe_async_xhr_upload(request, &completion_tx, internal_id);
|
||||
task_runner.spawn(async move {
|
||||
if let Some(preload) = loader.consume_document_preload(&request) {
|
||||
let completion = preload.into_completion(internal_id, request).await;
|
||||
let _ = completion_tx.send_async_subresource(completion);
|
||||
return;
|
||||
}
|
||||
let preflight_observer =
|
||||
CorsPreflightNetworkObserver::new(completion_tx.clone(), network_context);
|
||||
let auth_requires_buffered_transport = request.auth_requires_buffered_transport();
|
||||
@@ -1090,6 +1106,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
true,
|
||||
|
||||
@@ -188,6 +188,7 @@ fn cors_redirect_referrer_never_recovers_stripped_information() -> Result<()> {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -241,6 +242,7 @@ fn cors_redirect_modes_precede_location_parsing() -> Result<()> {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let result = redirects.advance(head, false, None);
|
||||
|
||||
@@ -130,6 +130,7 @@ pub(super) fn blob_response(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_bytes.to_vec(),
|
||||
@@ -154,6 +155,7 @@ pub(crate) fn data_url_response(url: &url::Url) -> Option<Response> {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_bytes,
|
||||
|
||||
@@ -115,8 +115,8 @@ pub(crate) fn start_scanned_image_preload(
|
||||
let decode_runner = task_runner.clone();
|
||||
let cancel_handle = load.cancel_handle();
|
||||
task_runner.spawn(async move {
|
||||
let result = match loader
|
||||
.fetch_raw_stream_with_cancel_and_network_metadata(request, cancel_handle)
|
||||
let result = match crate::network_host::fetch_browser_subresource_raw_stream_with_preflight_headers_and_network_metadata(
|
||||
&loader, request, Some(cancel_handle), Vec::new())
|
||||
.await
|
||||
{
|
||||
Ok(observed) => {
|
||||
|
||||
@@ -627,6 +627,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
}
|
||||
}
|
||||
@@ -659,6 +660,7 @@ mod tests {
|
||||
}],
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let document_url = url("https://document.test/page.html");
|
||||
|
||||
@@ -24,6 +24,9 @@ impl FetchResponseRequest<'_> {
|
||||
head: &moli_fetch::ResponseHead,
|
||||
credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
) -> crate::types::AsyncSubresourceFetchResponseFilter {
|
||||
if let Some(filter) = head.preload_state.response_filter() {
|
||||
return filter.clone();
|
||||
}
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter as Filter;
|
||||
let filter = response_filter(request_origin, head, self);
|
||||
match compute_fetch_response_type(request_origin, head, filter) {
|
||||
@@ -123,6 +126,10 @@ pub(crate) fn network_response_filter(
|
||||
let request_origin = request_origin.into();
|
||||
use crate::types::AsyncSubresourceFetchResponseFilter;
|
||||
|
||||
if let Some(filter) = head.preload_state.response_filter() {
|
||||
return Some(filter.clone());
|
||||
}
|
||||
|
||||
if redirect_mode == RequestRedirectMode::Manual && is_redirect_status(head.status) {
|
||||
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
|
||||
} else if request_mode == RequestMode::NoCors
|
||||
|
||||
@@ -96,6 +96,7 @@ fn xhr_complete_callback(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
pending_body_value,
|
||||
|
||||
@@ -352,11 +352,13 @@ pub(crate) async fn load_prepared_script_source_outcome_with_document_character_
|
||||
)
|
||||
}
|
||||
Err(error) => {
|
||||
let consumed_preload =
|
||||
error.is::<crate::network::preloads::ConsumedPreloadError>();
|
||||
let error = format!("failed to fetch script `{}`: {error}", script.url);
|
||||
PreparedScriptSourceLoadOutcome {
|
||||
source_result: Err(error.clone()),
|
||||
source_bytes: None,
|
||||
network_result: Some(Arc::new(Err(error))),
|
||||
network_result: (!consumed_preload).then(|| Arc::new(Err(error))),
|
||||
muted_errors: false,
|
||||
}
|
||||
}
|
||||
@@ -450,11 +452,12 @@ pub(crate) async fn load_service_worker_aware_external_script_source_outcome(
|
||||
.await
|
||||
}
|
||||
Err(error) => {
|
||||
let consumed_preload = error.is::<crate::network::preloads::ConsumedPreloadError>();
|
||||
let message = format!("failed to fetch script `{}`: {error}", script.url);
|
||||
PreparedScriptSourceLoadOutcome {
|
||||
source_result: Err(message.clone()),
|
||||
source_bytes: None,
|
||||
network_result: Some(Arc::new(Err(message))),
|
||||
network_result: (!consumed_preload).then(|| Arc::new(Err(message))),
|
||||
muted_errors: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ impl DetachedParserScriptFetchContinuation {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
ResponseBody::materialized_text(text, response_body),
|
||||
|
||||
@@ -256,6 +256,17 @@ impl RendererBrowserContextRuntime {
|
||||
destination: ServiceWorkerRequestDestination,
|
||||
resource_type: SubresourceResourceType,
|
||||
) -> Result<Option<ServiceWorkerDirectFetchResponse>> {
|
||||
if let Some(preload) = request_client.consume_document_preload(request) {
|
||||
let preload = preload
|
||||
.response()
|
||||
.await
|
||||
.map_err(crate::network::preloads::ConsumedPreloadError)?;
|
||||
return Ok(Some(ServiceWorkerDirectFetchResponse {
|
||||
response: Box::new(preload.response),
|
||||
response_filter: preload.response_filter,
|
||||
from_network_fallback: !preload.from_service_worker,
|
||||
}));
|
||||
}
|
||||
if !matches!(request.url.scheme(), "http" | "https") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -733,6 +733,7 @@ fn dedicated_worker_navigation_response_eq(
|
||||
&& left.redirect_chain == right.redirect_chain
|
||||
&& left.from_cache == right.from_cache
|
||||
&& left.cache_state == right.cache_state
|
||||
&& left.preload_state == right.preload_state
|
||||
&& left.negotiated_http_version == right.negotiated_http_version
|
||||
&& left.network_request_headers() == right.network_request_headers()
|
||||
}
|
||||
|
||||
@@ -492,6 +492,7 @@ fn about_blank_navigation_response(url: &Url) -> Option<moli_fetch::Response> {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
Vec::new(),
|
||||
|
||||
@@ -246,6 +246,7 @@ fetch({:?})
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
},
|
||||
@@ -462,6 +463,7 @@ async fn streaming_finish_requires_matching_request_and_body_source_identity() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -739,6 +739,7 @@ pub(super) fn stale_loaded_completion(
|
||||
response_body: None,
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
}),
|
||||
},
|
||||
))),
|
||||
|
||||
@@ -634,6 +634,7 @@ fn module_graph_network_result_records_staged_response_started_with_cache_state(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: true,
|
||||
cache_state: moli_fetch::ResponseCacheState::Local,
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"export default 1;".to_owned(),
|
||||
|
||||
@@ -331,6 +331,7 @@ enum ImageSubresourceTerminal<'a> {
|
||||
encoded: &'a moli_parkable_image::ParkableImage,
|
||||
},
|
||||
Failure,
|
||||
PreloadFailure,
|
||||
}
|
||||
|
||||
impl ImageSubresourceTerminal<'_> {
|
||||
@@ -348,7 +349,7 @@ impl ImageSubresourceTerminal<'_> {
|
||||
encoded.len(),
|
||||
)
|
||||
}
|
||||
Self::Failure => {
|
||||
Self::Failure | Self::PreloadFailure => {
|
||||
crate::context_bootstrap::ResourcePerformanceEntry::from_network_failure(
|
||||
request_url.as_str(),
|
||||
"img",
|
||||
@@ -383,7 +384,7 @@ fn apply_image_subresource_terminal(
|
||||
);
|
||||
(completion.accepted(), completion.followup())
|
||||
}
|
||||
ImageSubresourceTerminal::Failure => {
|
||||
ImageSubresourceTerminal::Failure | ImageSubresourceTerminal::PreloadFailure => {
|
||||
let followup = context_host
|
||||
.borrow_mut()
|
||||
.complete_pending_image_load_network_request_if_matches(
|
||||
@@ -395,7 +396,7 @@ fn apply_image_subresource_terminal(
|
||||
(followup.is_some(), followup)
|
||||
}
|
||||
};
|
||||
if accepted {
|
||||
if accepted && !matches!(terminal, ImageSubresourceTerminal::PreloadFailure) {
|
||||
crate::context_bootstrap::record_resource_performance_entry(
|
||||
scope,
|
||||
terminal.resource_performance_entry(request_url),
|
||||
|
||||
@@ -411,12 +411,21 @@ impl ScriptVm {
|
||||
network_error_text: Option<String>,
|
||||
completion_result: AsyncSubresourceFetchResult,
|
||||
) -> Result<AsyncSubresourceFetchBodyActivity> {
|
||||
let consumed_preload = match &completion_result {
|
||||
AsyncSubresourceFetchResult::PreloadFailure(_) => true,
|
||||
AsyncSubresourceFetchResult::Response(response)
|
||||
| AsyncSubresourceFetchResult::Image { response, .. } => {
|
||||
response.preload_state.is_consumed()
|
||||
}
|
||||
AsyncSubresourceFetchResult::Failure(_) => false,
|
||||
};
|
||||
let (supplied_parkable_image, result) = match completion_result {
|
||||
AsyncSubresourceFetchResult::Response(response) => (None, Ok(response)),
|
||||
AsyncSubresourceFetchResult::Image { response, encoded } => {
|
||||
(Some(encoded), Ok(response))
|
||||
}
|
||||
AsyncSubresourceFetchResult::Failure(error) => (None, Err(error)),
|
||||
AsyncSubresourceFetchResult::Failure(error)
|
||||
| AsyncSubresourceFetchResult::PreloadFailure(error) => (None, Err(error)),
|
||||
};
|
||||
let trace_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
|
||||
let trace_fields = async_subresource_trace_fields_for_pending(
|
||||
@@ -889,7 +898,7 @@ impl ScriptVm {
|
||||
sequence,
|
||||
pending.info.internal_id,
|
||||
&pending.info.url,
|
||||
ImageSubresourceTerminal::Failure,
|
||||
if consumed_preload { ImageSubresourceTerminal::PreloadFailure } else { ImageSubresourceTerminal::Failure },
|
||||
),
|
||||
PendingSubresourceContinuation::Media {
|
||||
media_handle,
|
||||
|
||||
@@ -860,6 +860,7 @@ impl ScriptVm {
|
||||
response_body: SubresourceResponseBody::from_navigation_response(&response),
|
||||
from_cache: response.from_cache,
|
||||
cache_state: response.cache_state,
|
||||
preload_state: response.preload_state.clone(),
|
||||
};
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
@@ -1117,6 +1118,7 @@ impl ScriptVm {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let pending = match continuation {
|
||||
@@ -1864,6 +1866,7 @@ impl ScriptVm {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: pending.response.from_cache,
|
||||
cache_state: pending.response.cache_state,
|
||||
preload_state: pending.response.preload_state.clone(),
|
||||
negotiated_http_version: pending.response.negotiated_http_version,
|
||||
});
|
||||
let result = if pending.pending.request_mode == moli_fetch::RequestMode::NoCors {
|
||||
@@ -1929,6 +1932,7 @@ impl ScriptVm {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: pending.response.from_cache,
|
||||
cache_state: pending.response.cache_state,
|
||||
preload_state: pending.response.preload_state.clone(),
|
||||
negotiated_http_version: pending.response.negotiated_http_version,
|
||||
});
|
||||
let result = if pending.pending.request_mode == moli_fetch::RequestMode::NoCors {
|
||||
@@ -2003,6 +2007,7 @@ impl ScriptVm {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: pending.response.from_cache,
|
||||
cache_state: pending.response.cache_state,
|
||||
preload_state: pending.response.preload_state.clone(),
|
||||
negotiated_http_version: pending.response.negotiated_http_version,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -139,6 +139,7 @@ impl ScriptVm {
|
||||
response_body: SubresourceResponseBody::from_navigation_response(&response),
|
||||
response_from_cache: response.from_cache,
|
||||
response_cache_state: response.cache_state,
|
||||
response_preload_state: response.preload_state.clone(),
|
||||
};
|
||||
trace_async_subresource_stage(
|
||||
"async_subresource_complete_running_auth_required",
|
||||
@@ -193,6 +194,7 @@ impl ScriptVm {
|
||||
response_body: SubresourceResponseBody::from_navigation_response(&response),
|
||||
from_cache: response.from_cache,
|
||||
cache_state: response.cache_state,
|
||||
preload_state: response.preload_state.clone(),
|
||||
};
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
|
||||
@@ -589,6 +589,7 @@ fn parser_script_network_results_populate_buffered_resource_timing_snapshots() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"void 0;".to_owned(),
|
||||
|
||||
@@ -180,6 +180,7 @@ fn xhr_null_body_statuses_discard_buffered_and_streamed_bytes() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
if streaming {
|
||||
@@ -346,6 +347,7 @@ fn xhr_null_body_late_completion_preserves_a_replacement_requests_progress() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
|
||||
@@ -65,6 +65,7 @@ fn window_service_worker_response_filter_survives_streaming_clone_and_cache() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let response_filter = Some(if cors {
|
||||
@@ -235,6 +236,7 @@ fn window_filtered_fetch_preserves_internal_head_through_clone_and_cache() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
if streaming {
|
||||
|
||||
@@ -58,6 +58,7 @@ fn redirect_filter_completion_keeps_status_text_and_explicit_filters() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"body".to_owned(),
|
||||
|
||||
@@ -36,6 +36,7 @@ fn xhr_response_headers_are_filtered_combined_and_sorted_before_events() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
if streaming {
|
||||
|
||||
@@ -1201,6 +1201,7 @@ fn xml_http_request_blob_response_uses_final_mime_type() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(b"body".to_vec()),
|
||||
@@ -1302,6 +1303,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(
|
||||
@@ -1330,6 +1332,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(b"plain text".to_vec()),
|
||||
@@ -1409,6 +1412,7 @@ fn xml_http_request_response_document_uses_response_url_and_requester_origin() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(br#"<html xmlns="http://www.w3.org/1999/xhtml"><head><base href="../assets/" /></head><body><a id="link" href="child">link</a></body></html>"#.to_vec()),
|
||||
@@ -1507,6 +1511,7 @@ fn xml_http_request_document_response_requires_an_eligible_mime_and_well_formed_
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
if streaming {
|
||||
@@ -1662,6 +1667,7 @@ fn xhr_streamed_response_documents_keep_distinct_source_modification_times() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
@@ -1780,6 +1786,7 @@ fn xhr_response_decoding_uses_headers_received_overrides_for_buffered_and_stream
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
if streaming {
|
||||
@@ -2966,6 +2973,7 @@ __streamingXhr.send();
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
|
||||
@@ -3153,6 +3161,7 @@ async fn streaming_subresource_finish_preserves_response_head_cache_state() {
|
||||
}],
|
||||
from_cache: true,
|
||||
cache_state: moli_fetch::ResponseCacheState::Local,
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
@@ -3434,6 +3443,7 @@ async fn streaming_fetch_body_error_records_response_started_then_body_failed()
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
@@ -3536,6 +3546,7 @@ fn install_streaming_fetch_response_fixture(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_source_id,
|
||||
@@ -3599,6 +3610,7 @@ fn install_streaming_fetch_response_fixture(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
@@ -3712,6 +3724,7 @@ async fn streaming_fetch_body_cancel_aborts_streaming_subresource() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
@@ -4350,6 +4363,7 @@ async fn streaming_xhr_materialization_failure_errors_body_source_before_close()
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
|
||||
@@ -656,6 +656,7 @@ async fn child_navigation_keeps_accepted_beacon_network_only_and_rejects_stale_s
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -326,6 +326,7 @@ async fn child_csp_report_keeps_exact_violation_document_without_v8_after_naviga
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1968,6 +1968,7 @@ fn isolated_realm_destruction_aborts_fetch_and_detaches_keepalive() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ fn fetch_request_initializers_use_request_header_guards() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
vm.complete_async_subresource_fetch(crate::types::AsyncSubresourceFetchCompletion {
|
||||
|
||||
@@ -375,6 +375,7 @@ fn register_pending_window_fetch_for_test(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
network_request_headers: None,
|
||||
|
||||
@@ -358,6 +358,7 @@ fn external_script_redirect_final_url_obeys_script_src_csp() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"globalThis.__redirectScriptRan = true;".to_owned(),
|
||||
|
||||
@@ -726,6 +726,7 @@ fn response_clone_tees_pending_network_body_after_parent_consumption() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_source_id,
|
||||
@@ -833,6 +834,7 @@ fn pending_fetch_body_pipe_through_text_decoder_stream_pulls_future_chunks() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_source_id,
|
||||
@@ -957,6 +959,7 @@ fn materialize_response_object_preserves_redirected_slot() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
"redirected-body".to_owned(),
|
||||
@@ -1340,6 +1343,7 @@ redirect_mode: moli_fetch::RequestRedirectMode::Follow,
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let id = crate::network_host::new_network_body_source_id();
|
||||
@@ -1444,6 +1448,7 @@ fn opaque_window_fetch_keeps_blocked_bytes_out_of_internal_clone_consumers() {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
},
|
||||
@@ -1557,6 +1562,7 @@ redirect_mode: moli_fetch::RequestRedirectMode::Follow, method: "GET", mode: mol
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
moli_fetch::ResponseBody::materialized_bytes(Vec::new()),
|
||||
|
||||
@@ -555,6 +555,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
|
||||
@@ -952,6 +953,7 @@ mod tests {
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let mut resource =
|
||||
|
||||
@@ -116,6 +116,7 @@ fn service_worker_fetch_stream_response_head(
|
||||
redirect_chain: job.request.redirect_chain().to_vec(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
}
|
||||
}
|
||||
@@ -640,6 +641,7 @@ impl ServiceWorkerRuntimeService {
|
||||
redirect_chain: job.request.redirect_chain().to_vec(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
String::from_utf8_lossy(&response.body).into_owned(),
|
||||
|
||||
@@ -21,22 +21,29 @@ struct ConnectedLinkReadinessFetchResponse {
|
||||
response: crate::protocol_types::NavigationResponse,
|
||||
origin_clean: bool,
|
||||
load_event_successful: bool,
|
||||
response_filter: Option<AsyncSubresourceFetchResponseFilter>,
|
||||
integrity_matches: bool,
|
||||
from_service_worker: bool,
|
||||
}
|
||||
|
||||
impl ConnectedLinkReadinessFetchResponse {
|
||||
fn from_response(
|
||||
response: crate::protocol_types::NavigationResponse,
|
||||
origin_clean: bool,
|
||||
response_filter: Option<AsyncSubresourceFetchResponseFilter>,
|
||||
load_event_successful: bool,
|
||||
integrity: Option<&str>,
|
||||
from_service_worker: bool,
|
||||
) -> Self {
|
||||
// Hash the original body, including binary responses. A matching digest
|
||||
// must not make an opaque response eligible, including SW responses to
|
||||
// an otherwise same-origin request.
|
||||
// Retain the network response even on integrity failure: its downloaded
|
||||
// bytes still contribute to the preload's Resource Timing entry.
|
||||
let load_event_successful = load_event_successful
|
||||
&& crate::subresource_integrity::response_matches_subresource_integrity_metadata(
|
||||
let origin_clean = response_filter
|
||||
.as_ref()
|
||||
.is_none_or(|filter| filter.is_readable());
|
||||
let integrity_matches =
|
||||
crate::subresource_integrity::response_matches_subresource_integrity_metadata(
|
||||
response.body_bytes(),
|
||||
integrity,
|
||||
origin_clean,
|
||||
@@ -44,9 +51,25 @@ impl ConnectedLinkReadinessFetchResponse {
|
||||
Self {
|
||||
response,
|
||||
origin_clean,
|
||||
load_event_successful,
|
||||
load_event_successful: load_event_successful && integrity_matches,
|
||||
response_filter,
|
||||
integrity_matches,
|
||||
from_service_worker,
|
||||
}
|
||||
}
|
||||
|
||||
fn preload_response(
|
||||
&self,
|
||||
) -> Result<crate::network::preloads::DocumentPreloadResponse, String> {
|
||||
if !self.integrity_matches {
|
||||
return Err("preload response failed its integrity check".to_owned());
|
||||
}
|
||||
Ok(crate::network::preloads::DocumentPreloadResponse {
|
||||
response: self.response.clone(),
|
||||
from_service_worker: self.from_service_worker,
|
||||
response_filter: self.response_filter.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1179,6 +1202,18 @@ impl DocumentRuntime {
|
||||
let start_unix_millis = moli_time::unix_epoch_millis();
|
||||
let resource_task_runner = resource_loader.task_runner();
|
||||
let request_origin = resource_loader.fetch_context().request_origin();
|
||||
let preload = fetch_options
|
||||
.link_preload
|
||||
.then(|| {
|
||||
let request = connected_link_readiness_request(
|
||||
&document_url,
|
||||
&request_origin,
|
||||
&url,
|
||||
&fetch_options,
|
||||
);
|
||||
loader.register_document_preload(&request)
|
||||
})
|
||||
.flatten();
|
||||
let document_owner = operation.document_owner();
|
||||
resource_loader.spawn_resource_task(async move {
|
||||
let result = fetch_connected_link_readiness_with_service_worker(
|
||||
@@ -1191,6 +1226,13 @@ impl DocumentRuntime {
|
||||
service_worker_context,
|
||||
)
|
||||
.await;
|
||||
let preload = preload.map(|producer| {
|
||||
let response = result
|
||||
.as_ref()
|
||||
.map_err(Clone::clone)
|
||||
.and_then(ConnectedLinkReadinessFetchResponse::preload_response);
|
||||
(producer, response)
|
||||
});
|
||||
let completion = ConnectedLoadCompletion {
|
||||
operation,
|
||||
successful: result
|
||||
@@ -1212,6 +1254,9 @@ impl DocumentRuntime {
|
||||
}],
|
||||
};
|
||||
let _ = task_producer.send_connected_completion(completion);
|
||||
if let Some((producer, response)) = preload {
|
||||
producer.complete(response);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -2302,19 +2347,16 @@ async fn fetch_connected_link_readiness_with_service_worker(
|
||||
.await
|
||||
{
|
||||
Ok(Some(response)) => {
|
||||
let origin_clean = response
|
||||
.response_filter
|
||||
.as_ref()
|
||||
.is_none_or(|filter| filter.is_readable());
|
||||
let load_event_successful = connected_link_load_event_successful(
|
||||
&response.response,
|
||||
response.response_filter,
|
||||
response.response_filter.clone(),
|
||||
);
|
||||
return Ok(ConnectedLinkReadinessFetchResponse::from_response(
|
||||
*response.response,
|
||||
origin_clean,
|
||||
response.response_filter,
|
||||
load_event_successful,
|
||||
integrity,
|
||||
!response.from_network_fallback,
|
||||
));
|
||||
}
|
||||
Ok(None) => {}
|
||||
@@ -2330,18 +2372,18 @@ async fn fetch_connected_link_readiness_with_service_worker(
|
||||
.await
|
||||
.map(|response| {
|
||||
let load_event_successful = connected_link_load_event_successful(&response, None);
|
||||
let origin_clean = crate::network_host::network_response_filter(
|
||||
let response_filter = crate::network_host::network_response_filter(
|
||||
&request_origin,
|
||||
&response.head(),
|
||||
request_mode,
|
||||
moli_fetch::RequestRedirectMode::Follow,
|
||||
)
|
||||
.is_none_or(|filter| filter.is_readable());
|
||||
);
|
||||
ConnectedLinkReadinessFetchResponse::from_response(
|
||||
response,
|
||||
origin_clean,
|
||||
response_filter,
|
||||
load_event_successful,
|
||||
integrity,
|
||||
false,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
use base64::Engine;
|
||||
use moli_crypto::DigestAlgorithm;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub(crate) struct IntegrityMetadataHash<'a> {
|
||||
pub(crate) algorithm: DigestAlgorithm,
|
||||
pub(crate) digest: &'a str,
|
||||
}
|
||||
|
||||
/// Preload consumption compares parsed metadata sets, including weaker hashes,
|
||||
/// while ignoring unknown algorithms, duplicate tokens, and unknown options.
|
||||
/// A consumer with no supported metadata can consume any matching preload,
|
||||
/// including one whose integrity check failed.
|
||||
pub(crate) fn integrity_metadata_allows_preload_consumption(
|
||||
preload: &str,
|
||||
consumer: Option<&str>,
|
||||
) -> bool {
|
||||
let consumer: Vec<_> = integrity_metadata_hashes(consumer.unwrap_or_default()).collect();
|
||||
if consumer.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let preload: Vec<_> = integrity_metadata_hashes(preload).collect();
|
||||
consumer.iter().all(|hash| preload.contains(hash))
|
||||
&& preload.iter().all(|hash| consumer.contains(hash))
|
||||
}
|
||||
|
||||
struct ParsedIntegrityMetadata {
|
||||
tokens: Vec<ParsedIntegrityToken>,
|
||||
}
|
||||
@@ -191,6 +209,33 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preload_integrity_compares_sets_including_weaker_hashes() {
|
||||
let preload = "sha384-AAAA sha256-BBBB";
|
||||
for compatible in [
|
||||
None,
|
||||
Some(""),
|
||||
Some("unknown-AAAA"),
|
||||
Some("sha256-BBBB sha384-AAAA"),
|
||||
Some("sha384-AAAA?ignored sha256-BBBB sha384-AAAA unknown-AAAA"),
|
||||
] {
|
||||
assert!(
|
||||
integrity_metadata_allows_preload_consumption(preload, compatible),
|
||||
"{compatible:?}"
|
||||
);
|
||||
}
|
||||
for incompatible in ["sha384-AAAA", "sha384-AAAA sha256-CCCC", "sha512-AAAA"] {
|
||||
assert!(
|
||||
!integrity_metadata_allows_preload_consumption(preload, Some(incompatible)),
|
||||
"{incompatible}"
|
||||
);
|
||||
}
|
||||
assert!(!integrity_metadata_allows_preload_consumption(
|
||||
"",
|
||||
Some("sha384-AAAA")
|
||||
));
|
||||
}
|
||||
|
||||
fn response_body_matches_subresource_integrity_metadata(
|
||||
body: &[u8],
|
||||
integrity: Option<&str>,
|
||||
|
||||
@@ -673,6 +673,8 @@ pub(super) enum AsyncSubresourceFetchResult {
|
||||
encoded: moli_parkable_image::ParkableImage,
|
||||
},
|
||||
Failure(String),
|
||||
/// The preload producer already reported this failed fetch.
|
||||
PreloadFailure(String),
|
||||
}
|
||||
|
||||
impl AsyncSubresourceFetchResult {
|
||||
@@ -702,7 +704,7 @@ impl AsyncSubresourceFetchResult {
|
||||
pub(super) fn into_result(self) -> std::result::Result<NavigationResponse, String> {
|
||||
match self {
|
||||
Self::Response(response) | Self::Image { response, .. } => Ok(response),
|
||||
Self::Failure(error) => Err(error),
|
||||
Self::Failure(error) | Self::PreloadFailure(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +712,7 @@ impl AsyncSubresourceFetchResult {
|
||||
pub(super) fn as_ref(&self) -> std::result::Result<&NavigationResponse, &String> {
|
||||
match self {
|
||||
Self::Response(response) | Self::Image { response, .. } => Ok(response),
|
||||
Self::Failure(error) => Err(error),
|
||||
Self::Failure(error) | Self::PreloadFailure(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,19 +720,19 @@ impl AsyncSubresourceFetchResult {
|
||||
pub(super) fn encoded(&self) -> Option<&moli_parkable_image::ParkableImage> {
|
||||
match self {
|
||||
Self::Image { encoded, .. } => Some(encoded),
|
||||
Self::Response(_) | Self::Failure(_) => None,
|
||||
Self::Response(_) | Self::Failure(_) | Self::PreloadFailure(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn is_ok(&self) -> bool {
|
||||
!matches!(self, Self::Failure(_))
|
||||
!matches!(self, Self::Failure(_) | Self::PreloadFailure(_))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn err(self) -> Option<String> {
|
||||
match self {
|
||||
Self::Failure(error) => Some(error),
|
||||
Self::Failure(error) | Self::PreloadFailure(error) => Some(error),
|
||||
Self::Response(_) | Self::Image { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -739,14 +741,14 @@ impl AsyncSubresourceFetchResult {
|
||||
pub(super) fn expect(self, message: &str) -> NavigationResponse {
|
||||
match self {
|
||||
Self::Response(response) | Self::Image { response, .. } => response,
|
||||
Self::Failure(error) => panic!("{message}: {error:?}"),
|
||||
Self::Failure(error) | Self::PreloadFailure(error) => panic!("{message}: {error:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn expect_err(self, message: &str) -> String {
|
||||
match self {
|
||||
Self::Failure(error) => error,
|
||||
Self::Failure(error) | Self::PreloadFailure(error) => error,
|
||||
Self::Response(response) | Self::Image { response, .. } => {
|
||||
panic!("{message}: {response:?}")
|
||||
}
|
||||
@@ -763,19 +765,7 @@ impl From<std::result::Result<NavigationResponse, String>> for AsyncSubresourceF
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) enum AsyncSubresourceFetchResponseFilter {
|
||||
Basic,
|
||||
Cors(Vec<String>),
|
||||
Opaque,
|
||||
OpaqueRedirect,
|
||||
}
|
||||
|
||||
impl AsyncSubresourceFetchResponseFilter {
|
||||
pub(super) fn is_readable(&self) -> bool {
|
||||
matches!(self, Self::Basic | Self::Cors(_))
|
||||
}
|
||||
}
|
||||
pub(super) use moli_fetch::FetchResponseFilter as AsyncSubresourceFetchResponseFilter;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct SubresourcePolicyContext {
|
||||
|
||||
@@ -2678,6 +2678,7 @@ pub(in crate::worker) fn worker_response_from_body(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
})
|
||||
}
|
||||
@@ -3090,6 +3091,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
response_body,
|
||||
response_from_cache: response_head.from_cache,
|
||||
response_cache_state: response_head.cache_state,
|
||||
response_preload_state: response_head.preload_state.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -3125,6 +3127,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
|
||||
response_body: response_body.clone(),
|
||||
from_cache: response_head.from_cache,
|
||||
cache_state: response_head.cache_state,
|
||||
preload_state: response_head.preload_state.clone(),
|
||||
};
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: completion.response_filter.clone(),
|
||||
|
||||
@@ -1070,6 +1070,7 @@ pub(in crate::worker) fn drain_worker_xhr_completion(
|
||||
response_body,
|
||||
response_from_cache: response_head.from_cache,
|
||||
response_cache_state: response_head.cache_state,
|
||||
response_preload_state: response_head.preload_state.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -1106,6 +1107,7 @@ pub(in crate::worker) fn drain_worker_xhr_completion(
|
||||
response_body: response_body.clone(),
|
||||
from_cache: response_head.from_cache,
|
||||
cache_state: response_head.cache_state,
|
||||
preload_state: response_head.preload_state.clone(),
|
||||
};
|
||||
pending.paused_response = Some(PausedWorkerSubresourceResponse {
|
||||
response_filter: None,
|
||||
|
||||
@@ -3447,6 +3447,7 @@ fn start_service_worker_navigation_preload_response_in_context(
|
||||
redirect_chain: Vec::new(),
|
||||
from_cache: false,
|
||||
cache_state: Default::default(),
|
||||
preload_state: Default::default(),
|
||||
negotiated_http_version: None,
|
||||
};
|
||||
let response_object = build_navigation_preload_response_object_from_stream_for_request_mode(
|
||||
|
||||
Reference in New Issue
Block a user