Files
servo/components/net/tests/http_cache.rs
Simon Wülker e73c010bb1 Force callers to claim blob url before making a fetch request (#43746)
`blob` URLs have a implicit blob URL entry attached, which stores the
data contained in the blob. The specification requires this entry to be
resolved as the URL is parsed. We only resolve it inside `net` when
loading the URL. That causes problems if the blob entry has been revoked
in the meantime - see https://github.com/servo/servo/issues/25226.

Ideally we would want to resolve blobs at parse-time as required. But
because `ServoUrl` is such a fundamental type, I've not managed to do
this change without having to touch hundreds of files at once.

Thus, we now require passing a `UrlWithBlobClaim` instead of a
`ServoUrl` when `fetch`-ing. This type proves that the caller has
acquired the blob beforehand.

As a temporary escape hatch, I've added
`UrlWithBlobClaim::from_url_without_having_claimed_blob`. That method
logs a warning if its used unsafely. This method is currently used in
most places to keep this change small. Only workers now acquire the blob
beforehand.

Testing: A new test starts to pass
Part of https://github.com/servo/servo/issues/43326
Part of https://github.com/servo/servo/issues/25226

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
2026-04-06 14:21:55 +00:00

121 lines
4.4 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use http::header::{CONTENT_LENGTH, CONTENT_RANGE, EXPIRES, HeaderValue, RANGE};
use http::{HeaderMap, StatusCode};
use net::http_cache::{CacheKey, HttpCache, refresh};
use net_traits::blob_url_store::UrlWithBlobClaim;
use net_traits::request::{Referrer, RequestBuilder};
use net_traits::response::{Response, ResponseBody};
use net_traits::{ResourceFetchTiming, ResourceTimingType};
use servo_base::id::TEST_PIPELINE_ID;
use servo_url::ServoUrl;
use tokio::sync::mpsc::unbounded_channel as unbounded;
#[tokio::test]
async fn test_refreshing_resource_sets_done_chan_the_appropriate_value() {
let response_bodies = vec![
ResponseBody::Receiving(vec![]),
ResponseBody::Empty,
ResponseBody::Done(vec![]),
];
let url = ServoUrl::parse("https://servo.org").unwrap();
let request = RequestBuilder::new(
None,
UrlWithBlobClaim::new(url.clone(), None),
Referrer::NoReferrer,
)
.pipeline_id(Some(TEST_PIPELINE_ID))
.origin(url.origin())
.build();
let timing = ResourceFetchTiming::new(ResourceTimingType::Navigation);
let mut response = Response::new(url.clone(), timing);
// Expires header makes the response cacheable.
response
.headers
.insert(EXPIRES, HeaderValue::from_str("-10").unwrap());
let cache = HttpCache::default();
for body in response_bodies {
*response.body.lock() = body.clone();
// First, store the 'normal' response.
let mut resource = {
let guard = cache.get_or_guard(CacheKey::new(&request)).await;
guard.insert(&request, &response);
cache.get_or_guard(CacheKey::new(&request)).await
};
// Second, mutate the response into a 304 response, and refresh the stored one.
response.status = StatusCode::NOT_MODIFIED.into();
let (send, recv) = unbounded();
let mut done_chan = Some((send, recv));
let refreshed_response = refresh(
&request,
response.clone(),
&mut done_chan,
resource.try_as_mut().unwrap(),
);
// Ensure a resource was found, and refreshed.
assert!(refreshed_response.is_some());
match body {
ResponseBody::Receiving(_) => assert!(done_chan.is_some()),
ResponseBody::Empty | ResponseBody::Done(_) => assert!(done_chan.is_none()),
}
}
}
#[tokio::test]
async fn test_skip_incomplete_cache_for_range_request_with_no_end_bound() {
let actual_body_len = 10;
let incomplete_response_body = &[1, 2, 3, 4, 5];
let url = ServoUrl::parse("https://servo.org").unwrap();
let cache = HttpCache::default();
let mut headers = HeaderMap::new();
headers.insert(
RANGE,
HeaderValue::from_str(&format!("bytes={}-", 0)).unwrap(),
);
let request = RequestBuilder::new(
None,
UrlWithBlobClaim::new(url.clone(), None),
Referrer::NoReferrer,
)
.pipeline_id(Some(TEST_PIPELINE_ID))
.origin(url.origin())
.headers(headers)
.build();
// Store incomplete response to http_cache
let timing = ResourceFetchTiming::new(ResourceTimingType::Navigation);
let mut initial_incomplete_response = Response::new(url.clone(), timing);
*initial_incomplete_response.body.lock() =
ResponseBody::Done(incomplete_response_body.to_vec());
initial_incomplete_response.headers.insert(
CONTENT_RANGE,
HeaderValue::from_str(&format!(
"bytes 0-{}/{}",
actual_body_len - 1,
actual_body_len
))
.unwrap(),
);
initial_incomplete_response.headers.insert(
CONTENT_LENGTH,
HeaderValue::from_str(&format!("{}", actual_body_len)).unwrap(),
);
initial_incomplete_response
.headers
.insert(EXPIRES, HeaderValue::from_str("0").unwrap());
initial_incomplete_response.status = StatusCode::PARTIAL_CONTENT.into();
cache.store(&request, &initial_incomplete_response).await;
// Try to construct response from http_cache
let mut done_chan = None;
let consecutive_response = cache.construct_response(&request, &mut done_chan).await;
assert!(
consecutive_response.is_none(),
"Should not construct response from incomplete response!"
);
}