diff --git a/components/net/http_cache.rs b/components/net/http_cache.rs new file mode 100644 index 000000000000..cd0211399a0c --- /dev/null +++ b/components/net/http_cache.rs @@ -0,0 +1,655 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#![deny(missing_docs)] + +//! A memory cache implementing the logic specified in http://tools.ietf.org/html/rfc7234 +//! and . + +use fetch::methods::DoneChannel; +use http_loader::is_redirect_status; +use hyper::header; +use hyper::header::ContentType; +use hyper::header::Headers; +use hyper::method::Method; +use hyper::status::StatusCode; +use hyper_serde::Serde; +use net_traits::{Metadata, FetchMetadata}; +use net_traits::request::Request; +use net_traits::response::{HttpsState, Response, ResponseBody}; +use servo_config::prefs::PREFS; +use servo_url::ServoUrl; +use std::collections::HashMap; +use std::str; +use std::sync::{Arc, Mutex}; +use time; +use time::{Duration, Tm}; + + +/// The key used to differentiate requests in the cache. +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct CacheKey { + url: ServoUrl +} + +impl CacheKey { + fn new(request: Request) -> CacheKey { + CacheKey { + url: request.current_url().clone() + } + } + + fn from_servo_url(servo_url: &ServoUrl) -> CacheKey { + CacheKey { + url: servo_url.clone() + } + } + + /// Retrieve the URL associated with this key + pub fn url(&self) -> ServoUrl { + self.url.clone() + } +} + +/// A complete cached resource. +#[derive(Clone)] +struct CachedResource { + metadata: CachedMetadata, + request_headers: Arc>, + body: Arc>, + location_url: Option>, + https_state: HttpsState, + status: Option, + raw_status: Option<(u16, Vec)>, + url_list: Vec, + expires: Duration, + last_validated: Tm +} + +/// Metadata about a loaded resource, such as is obtained from HTTP headers. +#[derive(Clone)] +struct CachedMetadata { + /// Final URL after redirects. + pub final_url: ServoUrl, + /// MIME type / subtype. + pub content_type: Option>, + /// Character set. + pub charset: Option, + /// Headers + pub headers: Arc>, + /// HTTP Status + pub status: Option<(u16, Vec)> +} + +/// Wrapper around a cached response, including information on re-validation needs +pub struct CachedResponse { + /// The response constructed from the cached resource + pub response: Response, + /// The revalidation flag for the stored response + pub needs_validation: bool +} + +/// A memory cache. +pub struct HttpCache { + /// cached responses. + entries: HashMap>, +} + + +/// Determine if a given response is cacheable based on the initial metadata received. +/// Based on +fn response_is_cacheable(metadata: &Metadata) -> bool { + // TODO: if we determine that this cache should be considered shared: + // 1. check for absence of private response directive + // 2. check for absence of the Authorization header field. + let mut is_cacheable = false; + let headers = metadata.headers.as_ref().unwrap(); + if headers.has::() || + headers.has::() || + headers.has::() { + is_cacheable = true; + } + if let Some(&header::CacheControl(ref directive)) = headers.get::() { + for directive in directive.iter() { + match *directive { + header::CacheDirective::NoStore => return false, + header::CacheDirective::Public | header::CacheDirective::SMaxAge(_) + | header::CacheDirective::MaxAge(_) | header::CacheDirective::NoCache => is_cacheable = true, + _ => {}, + } + } + } + if let Some(&header::Pragma::NoCache) = headers.get::() { + return false; + } + is_cacheable +} + +/// Calculating Age +/// +fn calculate_response_age(response: &Response) -> Duration { + // TODO: follow the spec more closely (Date headers, request/response lag, ...) + if let Some(secs) = response.headers.get_raw("Age") { + let seconds_string = String::from_utf8_lossy(&secs[0]); + if let Ok(secs) = seconds_string.parse::() { + return Duration::seconds(secs); + } + } + Duration::seconds(0i64) +} + +/// Determine the expiry date from relevant headers, +/// or uses a heuristic if none are present. +fn get_response_expiry(response: &Response) -> Duration { + // Calculating Freshness Lifetime + let age = calculate_response_age(&response); + if let Some(&header::CacheControl(ref directives)) = response.headers.get::() { + let has_no_cache_directive = directives.iter().any(|directive| { + header::CacheDirective::NoCache == *directive + }); + if has_no_cache_directive { + // Requires validation on first use. + return Duration::seconds(0i64); + } else { + for directive in directives { + match *directive { + header::CacheDirective::SMaxAge(secs) | header::CacheDirective::MaxAge(secs) => { + let max_age = Duration::seconds(secs as i64); + if max_age < age { + return Duration::seconds(0i64); + } + return max_age - age; + }, + _ => (), + } + } + } + } + if let Some(&header::Expires(header::HttpDate(t))) = response.headers.get::() { + // store the period of time from now until expiry + let desired = t.to_timespec(); + let current = time::now().to_timespec(); + if desired > current { + return desired - current; + } else { + return Duration::seconds(0i64); + } + } else { + if let Some(_) = response.headers.get_raw("Expires") { + // Malformed Expires header, shouldn't be used to construct a valid response. + return Duration::seconds(0i64); + } + } + // Calculating Heuristic Freshness + // + if let Some((ref code, _)) = response.raw_status { + // + // Since presently we do not generate a Warning header field with a 113 warn-code, + // 24 hours minus response age is the max for heuristic calculation. + let max_heuristic = Duration::hours(24) - age; + let heuristic_freshness = if let Some(&header::LastModified(header::HttpDate(t))) = + // If the response has a Last-Modified header field, + // caches are encouraged to use a heuristic expiration value + // that is no more than some fraction of the interval since that time. + response.headers.get::() { + let last_modified = t.to_timespec(); + let current = time::now().to_timespec(); + // A typical setting of this fraction might be 10%. + let raw_heuristic_calc = (current - last_modified) / 10; + let result = if raw_heuristic_calc < max_heuristic { + raw_heuristic_calc + } else { + max_heuristic + }; + result + } else { + max_heuristic + }; + match *code { + 200 | 203 | 204 | 206 | 300 | 301 | 404 | 405 | 410 | 414 | 501 => { + // Status codes that are cacheable by default + return heuristic_freshness + }, + _ => { + // Other status codes can only use heuristic freshness if the public cache directive is present. + if let Some(&header::CacheControl(ref directives)) = response.headers.get::() { + let has_public_directive = directives.iter().any(|directive| { + header::CacheDirective::Public == *directive + }); + if has_public_directive { + return heuristic_freshness; + } + } + }, + } + } + // Requires validation upon first use as default. + Duration::seconds(0i64) +} + +/// Request Cache-Control Directives +/// +fn get_expiry_adjustment_from_request_headers(request: &Request, expires: Duration) -> Duration { + let directive_data = match request.headers.get_raw("cache-control") { + Some(data) => data, + None => return expires, + }; + let directives_string = String::from_utf8_lossy(&directive_data[0]); + for directive in directives_string.split(",") { + let mut directive_info = directive.split("="); + match (directive_info.next(), directive_info.next()) { + (Some("max-stale"), Some(sec_str)) => { + if let Ok(secs) = sec_str.parse::() { + return expires + Duration::seconds(secs); + } + }, + (Some("max-age"), Some(sec_str)) => { + if let Ok(secs) = sec_str.parse::() { + let max_age = Duration::seconds(secs); + if expires > max_age { + return Duration::min_value(); + } + return expires - max_age; + } + }, + (Some("min-fresh"), Some(sec_str)) => { + if let Ok(secs) = sec_str.parse::() { + let min_fresh = Duration::seconds(secs); + if expires < min_fresh { + return Duration::min_value(); + } + return expires - min_fresh; + } + }, + (Some("no-cache"), _) | (Some("no-store"), _) => return Duration::min_value(), + _ => {} + } + } + expires +} + +/// Create a CachedResponse from a request and a CachedResource. +fn create_cached_response(request: &Request, cached_resource: &CachedResource, cached_headers: &Headers) + -> CachedResponse { + let mut response = Response::new(cached_resource.metadata.final_url.clone()); + response.headers = cached_headers.clone(); + response.body = cached_resource.body.clone(); + response.location_url = cached_resource.location_url.clone(); + response.status = cached_resource.status.clone(); + response.raw_status = cached_resource.raw_status.clone(); + response.url_list = cached_resource.url_list.clone(); + response.https_state = cached_resource.https_state.clone(); + response.referrer = request.referrer.to_url().cloned(); + response.referrer_policy = request.referrer_policy.clone(); + let expires = cached_resource.expires; + let adjusted_expires = get_expiry_adjustment_from_request_headers(request, expires); + let now = Duration::seconds(time::now().to_timespec().sec); + let last_validated = Duration::seconds(cached_resource.last_validated.to_timespec().sec); + let time_since_validated = now - last_validated; + // TODO: take must-revalidate into account + // TODO: if this cache is to be considered shared, take proxy-revalidate into account + // + let has_expired = (adjusted_expires < time_since_validated) || + (adjusted_expires == time_since_validated); + CachedResponse { response: response, needs_validation: has_expired } +} + +/// Create a new resource, based on the bytes requested, and an existing resource, +/// with a status-code of 206. +fn create_resource_with_bytes_from_resource(bytes: &[u8], resource: &CachedResource) + -> CachedResource { + CachedResource { + metadata: resource.metadata.clone(), + request_headers: resource.request_headers.clone(), + body: Arc::new(Mutex::new(ResponseBody::Done(bytes.to_owned()))), + location_url: resource.location_url.clone(), + https_state: resource.https_state.clone(), + status: Some(StatusCode::PartialContent), + raw_status: Some((206, b"Partial Content".to_vec())), + url_list: resource.url_list.clone(), + expires: resource.expires.clone(), + last_validated: resource.last_validated.clone() + } +} + +/// Support for range requests . +fn handle_range_request(request: &Request, candidates: Vec<&CachedResource>, range_spec: &[header::ByteRangeSpec]) + -> Option { + let mut complete_cached_resources = candidates.iter().filter(|resource| { + match resource.raw_status { + Some((ref code, _)) => *code == 200, + None => false + } + }); + let partial_cached_resources = candidates.iter().filter(|resource| { + match resource.raw_status { + Some((ref code, _)) => *code == 206, + None => false + } + }); + match (range_spec.first().unwrap(), complete_cached_resources.next()) { + // TODO: take the full range spec into account. + // If we have a complete resource, take the request range from the body. + // When there isn't a complete resource available, we loop over cached partials, + // and see if any individual partial response can fulfill the current request for a bytes range. + // TODO: combine partials that in combination could satisfy the requested range? + // see . + // TODO: add support for complete and partial resources, + // whose body is in the ResponseBody::Receiving state. + (&header::ByteRangeSpec::FromTo(beginning, end), Some(ref complete_resource)) => { + if let ResponseBody::Done(ref body) = *complete_resource.body.lock().unwrap() { + let b = beginning as usize; + let e = end as usize + 1; + let requested = body.get(b..e); + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(bytes, complete_resource); + let cached_headers = new_resource.metadata.headers.lock().unwrap(); + let cached_response = create_cached_response(request, &new_resource, &*cached_headers); + return Some(cached_response); + } + } + }, + (&header::ByteRangeSpec::FromTo(beginning, end), None) => { + for partial_resource in partial_cached_resources { + let headers = partial_resource.metadata.headers.lock().unwrap(); + let content_range = headers.get::(); + let (res_beginning, res_end) = match content_range { + Some(&header::ContentRange( + header::ContentRangeSpec::Bytes { + range: Some((res_beginning, res_end)), .. })) => (res_beginning, res_end), + _ => continue, + }; + if res_beginning - 1 < beginning && res_end + 1 > end { + let resource_body = &*partial_resource.body.lock().unwrap(); + let requested = match resource_body { + &ResponseBody::Done(ref body) => { + let b = beginning as usize - res_beginning as usize; + let e = end as usize - res_beginning as usize + 1; + body.get(b..e) + }, + _ => continue, + }; + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(&bytes, partial_resource); + let cached_response = create_cached_response(request, &new_resource, &*headers); + return Some(cached_response); + } + } + } + }, + (&header::ByteRangeSpec::AllFrom(beginning), Some(ref complete_resource)) => { + if let ResponseBody::Done(ref body) = *complete_resource.body.lock().unwrap() { + let b = beginning as usize; + let requested = body.get(b..); + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(bytes, complete_resource); + let cached_headers = new_resource.metadata.headers.lock().unwrap(); + let cached_response = create_cached_response(request, &new_resource, &*cached_headers); + return Some(cached_response); + } + } + }, + (&header::ByteRangeSpec::AllFrom(beginning), None) => { + for partial_resource in partial_cached_resources { + let headers = partial_resource.metadata.headers.lock().unwrap(); + let content_range = headers.get::(); + let (res_beginning, res_end, total) = match content_range { + Some(&header::ContentRange( + header::ContentRangeSpec::Bytes { + range: Some((res_beginning, res_end)), + instance_length: Some(total) })) => (res_beginning, res_end, total), + _ => continue, + }; + if res_beginning < beginning && res_end == total - 1 { + let resource_body = &*partial_resource.body.lock().unwrap(); + let requested = match resource_body { + &ResponseBody::Done(ref body) => { + let from_byte = beginning as usize - res_beginning as usize; + body.get(from_byte..) + }, + _ => continue, + }; + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(&bytes, partial_resource); + let cached_response = create_cached_response(request, &new_resource, &*headers); + return Some(cached_response); + } + } + } + }, + (&header::ByteRangeSpec::Last(offset), Some(ref complete_resource)) => { + if let ResponseBody::Done(ref body) = *complete_resource.body.lock().unwrap() { + let from_byte = body.len() - offset as usize; + let requested = body.get(from_byte..); + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(bytes, complete_resource); + let cached_headers = new_resource.metadata.headers.lock().unwrap(); + let cached_response = create_cached_response(request, &new_resource, &*cached_headers); + return Some(cached_response); + } + } + }, + (&header::ByteRangeSpec::Last(offset), None) => { + for partial_resource in partial_cached_resources { + let headers = partial_resource.metadata.headers.lock().unwrap(); + let content_range = headers.get::(); + let (res_beginning, res_end, total) = match content_range { + Some(&header::ContentRange( + header::ContentRangeSpec::Bytes { + range: Some((res_beginning, res_end)), + instance_length: Some(total) })) => (res_beginning, res_end, total), + _ => continue, + }; + if (total - res_beginning) > (offset - 1 ) && (total - res_end) < offset + 1 { + let resource_body = &*partial_resource.body.lock().unwrap(); + let requested = match resource_body { + &ResponseBody::Done(ref body) => { + let from_byte = body.len() - offset as usize; + body.get(from_byte..) + }, + _ => continue, + }; + if let Some(bytes) = requested { + let new_resource = create_resource_with_bytes_from_resource(&bytes, partial_resource); + let cached_response = create_cached_response(request, &new_resource, &*headers); + return Some(cached_response); + } + } + } + } + } + None +} + + +impl HttpCache { + /// Create a new memory cache instance. + pub fn new() -> HttpCache { + HttpCache { + entries: HashMap::new() + } + } + + /// Constructing Responses from Caches. + /// + pub fn construct_response(&self, request: &Request) -> Option { + // TODO: generate warning headers as appropriate + if request.method != Method::Get { + // Only Get requests are cached, avoid a url based match for others. + return None; + } + let entry_key = CacheKey::new(request.clone()); + let resources = match self.entries.get(&entry_key) { + Some(ref resources) => resources.clone(), + None => return None, + }; + let mut candidates = vec![]; + for cached_resource in resources.iter() { + let mut can_be_constructed = true; + let cached_headers = cached_resource.metadata.headers.lock().unwrap(); + let original_request_headers = cached_resource.request_headers.lock().unwrap(); + if let Some(vary_data) = cached_headers.get_raw("Vary") { + // Calculating Secondary Keys with Vary + let vary_data_string = String::from_utf8_lossy(&vary_data[0]); + let vary_values = vary_data_string.split(",").map(|val| val.trim()); + for vary_val in vary_values { + // For every header name found in the Vary header of the stored response. + if vary_val == "*" { + // A Vary header field-value of "*" always fails to match. + can_be_constructed = false; + break; + } + match request.headers.get_raw(vary_val) { + Some(header_data) => { + // If the header is present in the request. + let request_header_data_string = String::from_utf8_lossy(&header_data[0]); + if let Some(original_header_data) = original_request_headers.get_raw(vary_val) { + // Check that the value of the nominated header field, + // in the original request, matches the value in the current request. + let original_request_header_data_string = + String::from_utf8_lossy(&original_header_data[0]); + if original_request_header_data_string != request_header_data_string { + can_be_constructed = false; + break; + } + } + }, + None => { + // If a header field is absent from a request, + // it can only match a stored response if those headers, + // were also absent in the original request. + can_be_constructed = original_request_headers.get_raw(vary_val).is_none(); + }, + } + if !can_be_constructed { + break; + } + } + } + if can_be_constructed { + candidates.push(cached_resource); + } + } + // Support for range requests + if let Some(&header::Range::Bytes(ref range_spec)) = request.headers.get::() { + return handle_range_request(request, candidates, &range_spec); + } else { + // Not a Range request. + if let Some(ref cached_resource) = candidates.first() { + // Returning the first response that can be constructed + // TODO: select the most appropriate one, using a known mechanism from a selecting header field, + // or using the Date header to return the most recent one. + let cached_headers = cached_resource.metadata.headers.lock().unwrap(); + let cached_response = create_cached_response(request, cached_resource, &*cached_headers); + return Some(cached_response); + } + } + None + } + + /// Freshening Stored Responses upon Validation. + /// + pub fn refresh(&mut self, request: &Request, response: Response, done_chan: &mut DoneChannel) -> Option { + assert!(response.status == Some(StatusCode::NotModified)); + let entry_key = CacheKey::new(request.clone()); + if let Some(cached_resources) = self.entries.get_mut(&entry_key) { + for cached_resource in cached_resources.iter_mut() { + let mut stored_headers = cached_resource.metadata.headers.lock().unwrap(); + // Received a response with 304 status code, in response to a request that matches a cached resource. + // 1. update the headers of the cached resource. + // 2. return a response, constructed from the cached resource. + stored_headers.extend(response.headers.iter()); + let mut constructed_response = Response::new(cached_resource.metadata.final_url.clone()); + constructed_response.headers = stored_headers.clone(); + constructed_response.body = cached_resource.body.clone(); + constructed_response.status = cached_resource.status.clone(); + constructed_response.https_state = cached_resource.https_state.clone(); + constructed_response.referrer = request.referrer.to_url().cloned(); + constructed_response.referrer_policy = request.referrer_policy.clone(); + constructed_response.raw_status = cached_resource.raw_status.clone(); + constructed_response.url_list = cached_resource.url_list.clone(); + // done_chan will have been set to Some by http_network_fetch, + // set it back to None since the response returned here replaces the 304 one from the network. + *done_chan = None; + cached_resource.expires = get_response_expiry(&constructed_response); + return Some(constructed_response); + } + } + None + } + + fn invalidate_for_url(&mut self, url: &ServoUrl) { + let entry_key = CacheKey::from_servo_url(url); + if let Some(cached_resources) = self.entries.get_mut(&entry_key) { + for cached_resource in cached_resources.iter_mut() { + cached_resource.expires = Duration::seconds(0i64); + } + } + } + + /// Invalidation. + /// + pub fn invalidate(&mut self, request: &Request, response: &Response) { + if let Some(&header::Location(ref location)) = response.headers.get::() { + if let Ok(url) = request.current_url().join(location) { + self.invalidate_for_url(&url); + } + } + // TODO: update hyper to use typed getter. + if let Some(url_data) = response.headers.get_raw("Content-Location") { + if let Ok(content_location) = str::from_utf8(&url_data[0]) { + if let Ok(url) = request.current_url().join(content_location) { + self.invalidate_for_url(&url); + } + } + } + self.invalidate_for_url(&request.url()); + } + + /// Storing Responses in Caches. + /// + pub fn store(&mut self, request: &Request, response: &Response) { + if PREFS.get("network.http-cache.disabled").as_boolean().unwrap_or(false) { + return + } + if request.method != Method::Get { + // Only Get requests are cached. + return + } + let entry_key = CacheKey::new(request.clone()); + let metadata = match response.metadata() { + Ok(FetchMetadata::Filtered { + filtered: _, + unsafe_: metadata }) | + Ok(FetchMetadata::Unfiltered(metadata)) => metadata, + _ => return, + }; + if !response_is_cacheable(&metadata) { + return; + } + let expiry = get_response_expiry(&response); + let cacheable_metadata = CachedMetadata { + final_url: metadata.final_url, + content_type: metadata.content_type, + charset: metadata.charset, + status: metadata.status, + headers: Arc::new(Mutex::new(response.headers.clone())) + }; + let entry_resource = CachedResource { + metadata: cacheable_metadata, + request_headers: Arc::new(Mutex::new(request.headers.clone())), + body: response.body.clone(), + location_url: response.location_url.clone(), + https_state: response.https_state.clone(), + status: response.status.clone(), + raw_status: response.raw_status.clone(), + url_list: response.url_list.clone(), + expires: expiry, + last_validated: time::now() + }; + let entry = self.entries.entry(entry_key).or_insert(vec![]); + entry.push(entry_resource); + } + +} diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 932318ce1ae9..b77652067267 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -13,6 +13,7 @@ use fetch::methods::{Data, DoneChannel, FetchContext, Target}; use fetch::methods::{is_cors_safelisted_request_header, is_cors_safelisted_method, main_fetch}; use flate2::read::{DeflateDecoder, GzDecoder}; use hsts::HstsList; +use http_cache::HttpCache; use hyper::Error as HttpError; use hyper::LanguageTag; use hyper::client::{Pool, Request as HyperRequest, Response as HyperResponse}; @@ -22,7 +23,7 @@ use hyper::header::{AccessControlMaxAge, AccessControlRequestHeaders}; use hyper::header::{AccessControlRequestMethod, AcceptEncoding, AcceptLanguage}; use hyper::header::{Authorization, Basic, CacheControl, CacheDirective}; use hyper::header::{ContentEncoding, ContentLength, Encoding, Header, Headers}; -use hyper::header::{Host, Origin as HyperOrigin, IfMatch, IfRange}; +use hyper::header::{Host, HttpDate, Origin as HyperOrigin, IfMatch, IfRange}; use hyper::header::{IfUnmodifiedSince, IfModifiedSince, IfNoneMatch, Location}; use hyper::header::{Pragma, Quality, QualityItem, Referer, SetCookie}; use hyper::header::{UserAgent, q, qitem}; @@ -45,6 +46,7 @@ use std::io::{self, Read, Write}; use std::iter::FromIterator; use std::mem; use std::ops::Deref; +use std::str::FromStr; use std::sync::RwLock; use std::sync::mpsc::{channel, Sender}; use std::thread; @@ -69,6 +71,7 @@ fn read_block(reader: &mut R) -> Result { pub struct HttpState { pub hsts_list: RwLock, pub cookie_jar: RwLock, + pub http_cache: RwLock, pub auth_cache: RwLock, pub ssl_client: OpensslClient, pub connector: Pool, @@ -80,6 +83,7 @@ impl HttpState { hsts_list: RwLock::new(HstsList::new()), cookie_jar: RwLock::new(CookieStorage::new(150)), auth_cache: RwLock::new(AuthCache::new()), + http_cache: RwLock::new(HttpCache::new()), ssl_client: ssl_client.clone(), connector: create_http_connector(ssl_client), } @@ -895,34 +899,35 @@ fn http_network_or_cache_fetch(request: &mut Request, let mut revalidating_flag = false; // Step 21 - // TODO have a HTTP cache to check for a completed response - let complete_http_response_from_cache: Option = None; - if http_request.cache_mode != CacheMode::NoStore && - http_request.cache_mode != CacheMode::Reload && - complete_http_response_from_cache.is_some() { - // TODO Substep 1 and 2. Select a response from HTTP cache. - - // Substep 3 - if let Some(ref response) = response { - revalidating_flag = response_needs_revalidation(&response); - }; - - // Substep 4 - if http_request.cache_mode == CacheMode::ForceCache || - http_request.cache_mode == CacheMode::OnlyIfCached { - // TODO pull response from HTTP cache - // response = http_request - } + if let Ok(http_cache) = context.state.http_cache.read() { + if let Some(response_from_cache) = http_cache.construct_response(&http_request) { + let response_headers = response_from_cache.response.headers.clone(); + // Substep 1, 2, 3, 4 + let (cached_response, needs_revalidation) = match (http_request.cache_mode, &http_request.mode) { + (CacheMode::ForceCache, _) => (Some(response_from_cache.response), false), + (CacheMode::OnlyIfCached, &RequestMode::SameOrigin) => (Some(response_from_cache.response), false), + (CacheMode::OnlyIfCached, _) | (CacheMode::NoStore, _) | (CacheMode::Reload, _) => (None, false), + (_, _) => (Some(response_from_cache.response), response_from_cache.needs_validation) + }; + if needs_revalidation { + revalidating_flag = true; + // Substep 5 + // TODO: find out why the typed header getter return None from the headers of cached responses. + if let Some(date_slice) = response_headers.get_raw("Last-Modified") { + let date_string = String::from_utf8_lossy(&date_slice[0]); + if let Ok(http_date) = HttpDate::from_str(&date_string) { + http_request.headers.set(IfModifiedSince(http_date)); + } + } + if let Some(entity_tag) = + response_headers.get_raw("ETag") { + http_request.headers.set_raw("If-None-Match", entity_tag.to_vec()); - if revalidating_flag { - // Substep 5 - // TODO set If-None-Match and If-Modified-Since according to cached - // response headers. - } else { - // Substep 6 - // TODO pull response from HTTP cache - // response = http_request - // response.cache_state = CacheState::Local; + } + } else { + // Substep 6 + response = cached_response; + } } } @@ -933,26 +938,37 @@ fn http_network_or_cache_fetch(request: &mut Request, return Response::network_error( NetworkError::Internal("Couldn't find response in cache".into())) } + } + // More Step 22 + if response.is_none() { // Substep 2 let forward_response = http_network_fetch(http_request, credentials_flag, done_chan, context); // Substep 3 if let Some((200...399, _)) = forward_response.raw_status { if !http_request.method.safe() { - // TODO Invalidate HTTP cache response + if let Ok(mut http_cache) = context.state.http_cache.write() { + http_cache.invalidate(&http_request, &forward_response); + } } } // Substep 4 if revalidating_flag && forward_response.status.map_or(false, |s| s == StatusCode::NotModified) { - // TODO update forward_response headers with cached response headers + if let Ok(mut http_cache) = context.state.http_cache.write() { + response = http_cache.refresh(&http_request, forward_response.clone(), done_chan); + } } // Substep 5 if response.is_none() { + if http_request.cache_mode != CacheMode::NoStore { + // Subsubstep 2, doing it first to avoid a clone of forward_response. + if let Ok(mut http_cache) = context.state.http_cache.write() { + http_cache.store(&http_request, &forward_response); + } + } // Subsubstep 1 response = Some(forward_response); - // Subsubstep 2 - // TODO: store http_request and forward_response in cache } } @@ -1170,7 +1186,9 @@ fn http_network_fetch(request: &Request, // Step 14 if !response.is_network_error() && request.cache_mode != CacheMode::NoStore { - // TODO update response in the HTTP cache for request + if let Ok(mut http_cache) = context.state.http_cache.write() { + http_cache.store(&request, &response); + } } // TODO this step isn't possible yet @@ -1368,11 +1386,6 @@ fn is_no_store_cache(headers: &Headers) -> bool { headers.has::() } -fn response_needs_revalidation(_response: &Response) -> bool { - // TODO this function - false -} - /// pub fn is_redirect_status(status: StatusCode) -> bool { match status { diff --git a/components/net/lib.rs b/components/net/lib.rs index 7869ff1bb06f..c77fce6c509e 100644 --- a/components/net/lib.rs +++ b/components/net/lib.rs @@ -48,6 +48,7 @@ mod data_loader; pub mod filemanager_thread; mod hosts; pub mod hsts; +pub mod http_cache; pub mod http_loader; pub mod image_cache; pub mod mime_classifier; diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index 9e1a3c9eee74..22366c562cd1 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -12,6 +12,7 @@ use fetch::cors_cache::CorsCache; use fetch::methods::{FetchContext, fetch}; use filemanager_thread::{FileManager, TFDProvider}; use hsts::HstsList; +use http_cache::HttpCache; use http_loader::{HttpState, http_redirect_fetch}; use hyper_serde::Serde; use ipc_channel::ipc::{self, IpcReceiver, IpcReceiverSet, IpcSender}; @@ -91,6 +92,7 @@ struct ResourceChannelManager { fn create_http_states(config_dir: Option<&Path>) -> (Arc, Arc) { let mut hsts_list = HstsList::from_servo_preload(); let mut auth_cache = AuthCache::new(); + let http_cache = HttpCache::new(); let mut cookie_jar = CookieStorage::new(150); if let Some(config_dir) = config_dir { read_json_from_file(&mut auth_cache, config_dir, "auth_cache.json"); @@ -109,6 +111,7 @@ fn create_http_states(config_dir: Option<&Path>) -> (Arc, Arc HyperHeaders { + let mut headers = HyperHeaders::new(); + headers.extend(self.header_list.borrow_mut().iter()); + headers + } + // https://fetch.spec.whatwg.org/#concept-header-extract-mime-type pub fn extract_mime_type(&self) -> Vec { self.header_list.borrow().get_raw("content-type").map_or(vec![], |v| v[0].clone()) diff --git a/components/script/dom/request.rs b/components/script/dom/request.rs index f0b28f59b69e..85998533ae95 100644 --- a/components/script/dom/request.rs +++ b/components/script/dom/request.rs @@ -339,6 +339,9 @@ impl Request { _ => {}, } + // Copy the headers list onto the headers of net_traits::Request + r.request.borrow_mut().headers = r.Headers().get_headers_list(); + // Step 32 let mut input_body = if let RequestInfo::Request(ref input_request) = input { let input_request_request = input_request.request.borrow(); @@ -459,17 +462,7 @@ fn normalize_method(m: &str) -> HttpMethod { // https://fetch.spec.whatwg.org/#concept-method fn is_method(m: &ByteString) -> bool { - match m.to_lower().as_str() { - Some("get") => true, - Some("head") => true, - Some("post") => true, - Some("put") => true, - Some("delete") => true, - Some("connect") => true, - Some("options") => true, - Some("trace") => true, - _ => false, - } + m.as_str().is_some() } // https://fetch.spec.whatwg.org/#forbidden-method diff --git a/components/script/fetch.rs b/components/script/fetch.rs index fe201f0105c5..1085a215b03e 100644 --- a/components/script/fetch.rs +++ b/components/script/fetch.rs @@ -60,6 +60,7 @@ fn request_init_from_request(request: NetTraitsRequest) -> NetTraitsRequestInit referrer_policy: request.referrer_policy, pipeline_id: request.pipeline_id, redirect_mode: request.redirect_mode, + cache_mode: request.cache_mode, ..NetTraitsRequestInit::default() } } diff --git a/resources/prefs.json b/resources/prefs.json index 1c5a5c929431..36469aebf7d0 100644 --- a/resources/prefs.json +++ b/resources/prefs.json @@ -66,6 +66,7 @@ "layout.text-orientation.enabled": false, "layout.viewport.enabled": false, "layout.writing-mode.enabled": false, + "network.http-cache.disabled": false, "network.mime.sniff": false, "session-history.max-length": 20, "shell.builtin-key-shortcuts.enabled": true, diff --git a/tests/wpt/metadata/MANIFEST.json b/tests/wpt/metadata/MANIFEST.json index 363461b3a524..eaff0e74356d 100644 --- a/tests/wpt/metadata/MANIFEST.json +++ b/tests/wpt/metadata/MANIFEST.json @@ -528584,7 +528584,7 @@ "support" ], "fetch/http-cache/cc-request.html": [ - "d4417b8fd444362a3f217d1c95d37811a608e1a7", + "2002d341679139428e164cfe916dd39b9b664a3e", "testharness" ], "fetch/http-cache/freshness.html": [ @@ -528592,7 +528592,7 @@ "testharness" ], "fetch/http-cache/heuristic.html": [ - "5b0d55f891cb2e235456cd65f4e9f63e07999410", + "63837026eb6085fc7d6220c3dcab200b4bcd1eca", "testharness" ], "fetch/http-cache/http-cache.js": [ @@ -528604,7 +528604,7 @@ "testharness" ], "fetch/http-cache/partial.html": [ - "243e57c39f9e45e3e2acf845b36f3a140e3763bc", + "685057fe8876321a5d42bcf1e7582e6f0b745f85", "testharness" ], "fetch/http-cache/resources/http-cache.py": [ @@ -528616,7 +528616,7 @@ "testharness" ], "fetch/http-cache/vary.html": [ - "fa9a2e0554671bf2de5826e66ac0ea73de28d530", + "45f337270cfa90932c7469802655e313367ac92f", "testharness" ], "fetch/nosniff/image.html": [ diff --git a/tests/wpt/metadata/cors/304.htm.ini b/tests/wpt/metadata/cors/304.htm.ini deleted file mode 100644 index 9b7b2c0cf465..000000000000 --- a/tests/wpt/metadata/cors/304.htm.ini +++ /dev/null @@ -1,14 +0,0 @@ -[304.htm] - type: testharness - [A 304 response with no CORS headers inherits from the stored response] - expected: FAIL - - [A 304 can expand Access-Control-Expose-Headers] - expected: FAIL - - [A 304 can contract Access-Control-Expose-Headers] - expected: FAIL - - [A 304 can change Access-Control-Allow-Origin] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/basic/accept-header.any.js.ini b/tests/wpt/metadata/fetch/api/basic/accept-header.any.js.ini deleted file mode 100644 index fba2dac22d30..000000000000 --- a/tests/wpt/metadata/fetch/api/basic/accept-header.any.js.ini +++ /dev/null @@ -1,17 +0,0 @@ -[accept-header.any.html] - type: testharness - [Request through fetch should have 'accept' header with value 'custom/*'] - expected: FAIL - - [Request through fetch should have 'accept-language' header with value 'bzh'] - expected: FAIL - - -[accept-header.any.worker.html] - type: testharness - [Request through fetch should have 'accept' header with value 'custom/*'] - expected: FAIL - - [Request through fetch should have 'accept-language' header with value 'bzh'] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/basic/request-headers.any.js.ini b/tests/wpt/metadata/fetch/api/basic/request-headers.any.js.ini index fc724ad68bdf..ea7c5b5edf76 100644 --- a/tests/wpt/metadata/fetch/api/basic/request-headers.any.js.ini +++ b/tests/wpt/metadata/fetch/api/basic/request-headers.any.js.ini @@ -39,12 +39,6 @@ [Fetch with Chicken with body] expected: FAIL - [Fetch with TacO and mode "same-origin" needs an Origin header] - expected: FAIL - - [Fetch with TacO and mode "cors" needs an Origin header] - expected: FAIL - [request-headers.any.worker.html] type: testharness diff --git a/tests/wpt/metadata/fetch/api/cors/cors-preflight-star.any.js.ini b/tests/wpt/metadata/fetch/api/cors/cors-preflight-star.any.js.ini index 385bc215a339..775345b3f73e 100644 --- a/tests/wpt/metadata/fetch/api/cors/cors-preflight-star.any.js.ini +++ b/tests/wpt/metadata/fetch/api/cors/cors-preflight-star.any.js.ini @@ -1,17 +1,8 @@ [cors-preflight-star.any.html] type: testharness - [CORS that succeeds with credentials: false; method: SUPER (allowed: *); header: X-Test,1 (allowed: x-test)] - expected: FAIL - [CORS that succeeds with credentials: false; method: OK (allowed: *); header: X-Test,1 (allowed: *)] expected: FAIL - [CORS that fails with credentials: true; method: GET (allowed: get); header: X-Test,1 (allowed: *)] - expected: FAIL - - [CORS that fails with credentials: true; method: GET (allowed: *); header: X-Test,1 (allowed: *)] - expected: FAIL - [CORS that succeeds with credentials: true; method: PUT (allowed: PUT); header: (allowed: *)] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini b/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini index 6729fb6e75e7..338a4ba3c2be 100644 --- a/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini +++ b/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini @@ -1,47 +1,17 @@ [cors-preflight.any.html] type: testharness - [CORS [PATCH\], server allows] - expected: FAIL - - [CORS [NEW\], server allows] - expected: FAIL - - [CORS [GET\] [x-test-header: allowed\], server allows] - expected: FAIL - - [CORS [GET\] [x-test-header: refused\], server refuses] - expected: FAIL - [CORS [GET\] [several headers\], server allows] expected: FAIL - [CORS [GET\] [several headers\], server refuses] - expected: FAIL - [CORS [PUT\] [several headers\], server allows] expected: FAIL [cors-preflight.any.worker.html] type: testharness - [CORS [PATCH\], server allows] - expected: FAIL - - [CORS [NEW\], server allows] - expected: FAIL - - [CORS [GET\] [x-test-header: allowed\], server allows] - expected: FAIL - - [CORS [GET\] [x-test-header: refused\], server refuses] - expected: FAIL - [CORS [GET\] [several headers\], server allows] expected: FAIL - [CORS [GET\] [several headers\], server refuses] - expected: FAIL - [CORS [PUT\] [several headers\], server allows] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/cors/cors-redirect-preflight.any.js.ini b/tests/wpt/metadata/fetch/api/cors/cors-redirect-preflight.any.js.ini deleted file mode 100644 index 7b17b5619f68..000000000000 --- a/tests/wpt/metadata/fetch/api/cors/cors-redirect-preflight.any.js.ini +++ /dev/null @@ -1,185 +0,0 @@ -[cors-redirect-preflight.any.html] - type: testharness - [Redirect 301: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 301: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 301: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - -[cors-redirect-preflight.any.worker.html] - type: testharness - [Redirect 301: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 301: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 301: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 301: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 302: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 302: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 303: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 303: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 307: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 307: cors to another cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: same origin to cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: same origin to cors (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: cors to same origin (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: cors to same origin (preflight after redirection failure case)] - expected: FAIL - - [Redirect 308: cors to another cors (preflight after redirection success case)] - expected: FAIL - - [Redirect 308: cors to another cors (preflight after redirection failure case)] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/credentials/authentication-basic.any.js.ini b/tests/wpt/metadata/fetch/api/credentials/authentication-basic.any.js.ini deleted file mode 100644 index 55038e25b7a3..000000000000 --- a/tests/wpt/metadata/fetch/api/credentials/authentication-basic.any.js.ini +++ /dev/null @@ -1,23 +0,0 @@ -[authentication-basic.any.html] - type: testharness - [User-added Authorization header with include mode] - expected: FAIL - - [User-added Authorization header with same-origin mode] - expected: FAIL - - [User-added Authorization header with omit mode] - expected: FAIL - - -[authentication-basic.any.worker.html] - type: testharness - [User-added Authorization header with include mode] - expected: FAIL - - [User-added Authorization header with same-origin mode] - expected: FAIL - - [User-added Authorization header with omit mode] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/request/request-cache-default-conditional.html.ini b/tests/wpt/metadata/fetch/api/request/request-cache-default-conditional.html.ini deleted file mode 100644 index 919c03caf2a5..000000000000 --- a/tests/wpt/metadata/fetch/api/request/request-cache-default-conditional.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[request-cache-default-conditional.html] - type: testharness - disabled: https://github.com/servo/servo/issues/13441 diff --git a/tests/wpt/metadata/fetch/api/request/request-cache-default.html.ini b/tests/wpt/metadata/fetch/api/request/request-cache-default.html.ini deleted file mode 100644 index 1363bbf5134f..000000000000 --- a/tests/wpt/metadata/fetch/api/request/request-cache-default.html.ini +++ /dev/null @@ -1,11 +0,0 @@ -[request-cache-default.html] - type: testharness - [RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists with Etag and fresh response] - expected: FAIL - - [RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists with date and fresh response] - expected: FAIL - - [RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists with Last-Modified and fresh response] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/request/request-cache-force-cache.html.ini b/tests/wpt/metadata/fetch/api/request/request-cache-force-cache.html.ini deleted file mode 100644 index 24d84797f315..000000000000 --- a/tests/wpt/metadata/fetch/api/request/request-cache-force-cache.html.ini +++ /dev/null @@ -1,29 +0,0 @@ -[request-cache-force-cache.html] - type: testharness - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses with Etag and stale response] - expected: FAIL - - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses with date and stale response] - expected: FAIL - - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses with Etag and fresh response] - expected: FAIL - - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses with date and fresh response] - expected: FAIL - - [RequestCache "force-cache" stores the response in the cache if it goes to the network with Etag and fresh response] - expected: FAIL - - [RequestCache "force-cache" stores the response in the cache if it goes to the network with date and fresh response] - expected: FAIL - - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses with Last-Modified and stale response] - expected: FAIL - - [RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "force-cache" stores the response in the cache if it goes to the network with Last-Modified and fresh response] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/request/request-cache-only-if-cached.html.ini b/tests/wpt/metadata/fetch/api/request/request-cache-only-if-cached.html.ini deleted file mode 100644 index a3406a375ccc..000000000000 --- a/tests/wpt/metadata/fetch/api/request/request-cache-only-if-cached.html.ini +++ /dev/null @@ -1,65 +0,0 @@ -[request-cache-only-if-cached.html] - type: testharness - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses with Etag and stale response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses with date and stale response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses with Etag and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses with date and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found with Etag and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found with date and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with Etag and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with date and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with Etag and stale response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with date and stale response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with Etag and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with date and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with Etag and stale response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with date and stale response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses with Last-Modified and stale response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content with Last-Modified and stale response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects with Last-Modified and stale response] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/request/request-cache-reload.html.ini b/tests/wpt/metadata/fetch/api/request/request-cache-reload.html.ini deleted file mode 100644 index 4538f3d560f8..000000000000 --- a/tests/wpt/metadata/fetch/api/request/request-cache-reload.html.ini +++ /dev/null @@ -1,20 +0,0 @@ -[request-cache-reload.html] - type: testharness - [RequestCache "reload" mode does store the response in the cache with Etag and fresh response] - expected: FAIL - - [RequestCache "reload" mode does store the response in the cache with date and fresh response] - expected: FAIL - - [RequestCache "reload" mode does store the response in the cache even if a previous response is already stored with Etag and fresh response] - expected: FAIL - - [RequestCache "reload" mode does store the response in the cache even if a previous response is already stored with date and fresh response] - expected: FAIL - - [RequestCache "reload" mode does store the response in the cache with Last-Modified and fresh response] - expected: FAIL - - [RequestCache "reload" mode does store the response in the cache even if a previous response is already stored with Last-Modified and fresh response] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/304-update.html.ini b/tests/wpt/metadata/fetch/http-cache/304-update.html.ini deleted file mode 100644 index d6a1386dd4c7..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/304-update.html.ini +++ /dev/null @@ -1,14 +0,0 @@ -[304-update.html] - type: testharness - [HTTP cache updates returned headers from a Last-Modified 304.] - expected: FAIL - - [HTTP cache updates stored headers from a Last-Modified 304.] - expected: FAIL - - [HTTP cache updates returned headers from a ETag 304.] - expected: FAIL - - [HTTP cache updates stored headers from a ETag 304.] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/cc-request.html.ini b/tests/wpt/metadata/fetch/http-cache/cc-request.html.ini index 253204fe7002..cc3c925009bb 100644 --- a/tests/wpt/metadata/fetch/http-cache/cc-request.html.ini +++ b/tests/wpt/metadata/fetch/http-cache/cc-request.html.ini @@ -1,11 +1,5 @@ [cc-request.html] type: testharness - [HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use.] - expected: FAIL - - [HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use.] - expected: FAIL - [HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached.] expected: FAIL diff --git a/tests/wpt/metadata/fetch/http-cache/freshness.html.ini b/tests/wpt/metadata/fetch/http-cache/freshness.html.ini deleted file mode 100644 index 28ad1e13bd36..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/freshness.html.ini +++ /dev/null @@ -1,20 +0,0 @@ -[freshness.html] - type: testharness - [HTTP cache reuses a response with a future Expires.] - expected: FAIL - - [HTTP cache reuses a response with positive Cache-Control: max-age.] - expected: FAIL - - [HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires.] - expected: FAIL - - [HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires.] - expected: FAIL - - [HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use.] - expected: FAIL - - [HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires.] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/heuristic.html.ini b/tests/wpt/metadata/fetch/http-cache/heuristic.html.ini deleted file mode 100644 index 9a1905eba23c..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/heuristic.html.ini +++ /dev/null @@ -1,29 +0,0 @@ -[heuristic.html] - type: testharness - [HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present.] - expected: FAIL - - [HTTP cache reuses a 200 OK response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 203 Non-Authoritative Information response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 204 No Content response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 404 Not Found response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 405 Method Not Allowed response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 410 Gone response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 414 URI Too Long response with Last-Modified based upon heuristic freshness.] - expected: FAIL - - [HTTP cache reuses a 501 Not Implemented response with Last-Modified based upon heuristic freshness.] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/invalidate.html.ini b/tests/wpt/metadata/fetch/http-cache/invalidate.html.ini deleted file mode 100644 index ccf74fbef316..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/invalidate.html.ini +++ /dev/null @@ -1,20 +0,0 @@ -[invalidate.html] - type: testharness - [HTTP cache does not invalidate after a failed response from an unsafe request] - expected: FAIL - - [HTTP cache invalidates after a successful response from an unknown method] - expected: FAIL - - [HTTP cache does not invalidate Location URL after a failed response from an unsafe request] - expected: FAIL - - [HTTP cache invalidates Location URL after a successful response from an unknown method] - expected: FAIL - - [HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request] - expected: FAIL - - [HTTP cache invalidates Content-Location URL after a successful response from an unknown method] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/partial.html.ini b/tests/wpt/metadata/fetch/http-cache/partial.html.ini index e4afd89f90b3..65c7f3ebc0c7 100644 --- a/tests/wpt/metadata/fetch/http-cache/partial.html.ini +++ b/tests/wpt/metadata/fetch/http-cache/partial.html.ini @@ -1,13 +1,5 @@ [partial.html] type: testharness - [HTTP cache stores partial content and reuses it.] - expected: FAIL - - [HTTP cache stores complete response and serves smaller ranges from it.] - expected: FAIL - - [HTTP cache stores partial response and serves smaller ranges from it.] - expected: FAIL [HTTP cache stores partial content and completes it.] expected: FAIL diff --git a/tests/wpt/metadata/fetch/http-cache/status.html.ini b/tests/wpt/metadata/fetch/http-cache/status.html.ini deleted file mode 100644 index c5990d83543b..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/status.html.ini +++ /dev/null @@ -1,41 +0,0 @@ -[status.html] - type: testharness - [HTTP cache avoids going to the network if it has a fresh 200 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 203 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 204 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 299 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 400 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 404 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 410 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 499 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 500 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 502 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 503 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 504 response.] - expected: FAIL - - [HTTP cache avoids going to the network if it has a fresh 599 response.] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/http-cache/vary.html.ini b/tests/wpt/metadata/fetch/http-cache/vary.html.ini deleted file mode 100644 index fe697287a210..000000000000 --- a/tests/wpt/metadata/fetch/http-cache/vary.html.ini +++ /dev/null @@ -1,17 +0,0 @@ -[vary.html] - type: testharness - [HTTP cache reuses Vary response when request matches.] - expected: FAIL - - [HTTP cache doesn't invalidate existing Vary response.] - expected: FAIL - - [HTTP cache doesn't pay attention to headers not listed in Vary.] - expected: FAIL - - [HTTP cache reuses two-way Vary response when request matches.] - expected: FAIL - - [HTTP cache reuses three-way Vary response when request matches.] - expected: FAIL - diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 5d720d322093..26c3042306c4 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12466,6 +12466,11 @@ {} ] ], + "mozilla/resources/http-cache.js": [ + [ + {} + ] + ], "mozilla/resources/iframe_contentDocument_inner.html": [ [ {} @@ -33218,6 +33223,12 @@ {} ] ], + "mozilla/http-cache.html": [ + [ + "/_mozilla/mozilla/http-cache.html", + {} + ] + ], "mozilla/iframe-unblock-onload.html": [ [ "/_mozilla/mozilla/iframe-unblock-onload.html", @@ -66309,6 +66320,10 @@ "592f69ee432ba5bc7a2f2649e72e083d21393496", "testharness" ], + "mozilla/http-cache.html": [ + "33827dc9bdb0efcbcae4f730086693be315cfc14", + "testharness" + ], "mozilla/iframe-unblock-onload.html": [ "8734756947d36b047df256f27adc56fce7e31f88", "testharness" @@ -71981,6 +71996,10 @@ "78686147f85e4146e7fc58c1f67a613f65b099a2", "support" ], + "mozilla/resources/http-cache.js": [ + "c6b1ee9def26d4e12a1b93e551c225f82b4717c2", + "support" + ], "mozilla/resources/iframe_contentDocument_inner.html": [ "eb1b1ae3bb7a437fc4fbdd1f537881890fe6347c", "support" diff --git a/tests/wpt/mozilla/meta/mozilla/http-cache.html.ini b/tests/wpt/mozilla/meta/mozilla/http-cache.html.ini new file mode 100644 index 000000000000..1510dd19f988 --- /dev/null +++ b/tests/wpt/mozilla/meta/mozilla/http-cache.html.ini @@ -0,0 +1,3 @@ +[http-cache.html] + type: testharness + prefs: [network.http-cache.disabled:true] diff --git a/tests/wpt/mozilla/tests/mozilla/http-cache.html b/tests/wpt/mozilla/tests/mozilla/http-cache.html new file mode 100644 index 000000000000..3254492e89a5 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/http-cache.html @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/tests/wpt/mozilla/tests/mozilla/resources/http-cache.js b/tests/wpt/mozilla/tests/mozilla/resources/http-cache.js new file mode 100644 index 000000000000..34aaacf536f3 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/resources/http-cache.js @@ -0,0 +1,286 @@ +/** + * Each test run gets its own URL and randomized content and operates independently. + * + * Tests are an array of objects, each representing a request to make and check. + * The cache.py server script stashes an entry containing observed headers for + * each request it receives. When the test fetches have run, this state is retrieved + * and the expected_* lists are checked, including their length. + * + * Request object keys: + * - template - A template object for the request, by name -- see "templates" below. + * - request_method - A string containing the HTTP method to be used. + * - request_headers - An array of [header_name_string, header_value_string] arrays to + * emit in the request. + * - request_body - A string to use as the request body. + * - mode - The mode string to pass to fetch(). + * - credentials - The credentials string to pass to fetch(). + * - cache - The cache string to pass to fetch(). + * - pause_after - Boolean controlling a 3-second pause after the request completes. + * - response_status - A [number, string] array containing the HTTP status code + * and phrase to return. + * - response_headers - An array of [header_name_string, header_value_string] arrays to + * emit in the response. These values will also be checked like + * expected_response_headers, unless there is a third value that is + * false. + * - response_body - String to send as the response body. If not set, it will contain + * the test identifier. + * - expected_type - One of ["cached", "not_cached", "lm_validate", "etag_validate", "error"] + * - expected_status - A number representing a HTTP status code to check the response for. + * If not set, the value of response_status[0] will be used; if that + * is not set, 200 will be used. + * - expected_request_headers - An array of [header_name_string, header_value_string] representing + * headers to check the request for. + * - expected_response_headers - An array of [header_name_string, header_value_string] representing + * headers to check the response for. See also response_headers. + * - expected_response_text - A string to check the response body against. + */ + +function make_url(uuid, requests, idx) { + var arg = ""; + if ("query_arg" in requests[idx]) { + arg = "&target=" + requests[idx].query_arg; + } + return "/fetch/http-cache/resources/http-cache.py?token=" + uuid + "&info=" + btoa(JSON.stringify(requests)) + arg; +} + +function server_state(uuid) { + return fetch("/fetch/http-cache/resources/http-cache.py?querystate&token=" + uuid) + .then(function(response) { + return response.text(); + }).then(function(text) { + // null will be returned if the server never received any requests + // for the given uuid. Normalize that to an empty list consistent + // with our representation. + return JSON.parse(text) || []; + }); +} + + +templates = { + "fresh": { + "response_headers": [ + ['Expires', http_date(100000)], + ['Last-Modified', http_date(0)] + ] + }, + "stale": { + "response_headers": [ + ['Expires', http_date(-5000)], + ['Last-Modified', http_date(-100000)] + ] + }, + "lcl_response": { + "response_headers": [ + ['Location', "location_target"], + ['Content-Location', "content_location_target"] + ] + }, + "location": { + "query_arg": "location_target", + "response_headers": [ + ['Expires', http_date(100000)], + ['Last-Modified', http_date(0)] + ] + }, + "content_location": { + "query_arg": "content_location_target", + "response_headers": [ + ['Expires', http_date(100000)], + ['Last-Modified', http_date(0)] + ] + } +} + +function make_test(raw_requests) { + var requests = []; + for (var i = 0; i < raw_requests.length; i++) { + var request = raw_requests[i]; + if ("template" in request) { + var template = templates[request["template"]]; + for (var member in template) { + if (! request.hasOwnProperty(member)) { + request[member] = template[member]; + } + } + } + if ("expected_type" in request && request.expected_type === "cached") { + // requests after one that's expected to be cached will get out of sync + // with the server; not currently supported. + if (raw_requests.length > i + 1) { + assert_unreached("Making requests after something is expected to be cached."); + } + } + requests.push(request); + } + return function(test) { + var uuid = token(); + var fetch_functions = []; + for (var i = 0; i < requests.length; ++i) { + fetch_functions.push({ + code: function(idx) { + var init = {}; + var url = make_url(uuid, requests, idx); + var config = requests[idx]; + if ("request_method" in config) { + init.method = config["request_method"]; + } + if ("request_headers" in config) { + init.headers = config["request_headers"]; + } + if ("request_body" in config) { + init.body = config["request_body"]; + } + if ("mode" in config) { + init.mode = config["mode"]; + } + if ("credentials" in config) { + init.mode = config["credentials"]; + } + if ("cache" in config) { + init.cache = config["cache"]; + } + console.log(url, init) + return fetch(url, init.cache) + .then(function(response) { + var res_num = parseInt(response.headers.get("Server-Request-Count")); + var req_num = idx + 1; + if ("expected_type" in config) { + if (config.expected_type === "error") { + assert_true(false, "Request " + req_num + " should have been an error"); + return [response.text(), response_status]; + } + if (config.expected_type === "cached") { + assert_less_than(res_num, req_num, "Response used"); + } + if (config.expected_type === "not_cached") { + assert_equals(res_num, req_num, "Response used"); + } + } + if ("expected_status" in config) { + assert_equals(response.status, config.expected_status, "Response status"); + } else if ("response_status" in config) { + assert_equals(response.status, config.response_status[0], "Response status"); + } else { + assert_equals(response.status, 200, "Response status") + } + if ("response_headers" in config) { + config.response_headers.forEach(function(header) { + if (header.len < 3 || header[2] === true) { + assert_equals(response.headers.get(header[0]), header[1], "Response header") + } + }) + } + if ("expected_response_headers" in config) { + config.expected_response_headers.forEach(function(header) { + assert_equals(response.headers.get(header[0]), header[1], "Response header"); + }); + } + return response.text(); + }).then(function(res_body) { + if ("expected_response_text" in config) { + assert_equals(res_body, config.expected_response_text, "Response body"); + } else if ("response_body" in config) { + assert_equals(res_body, config.response_body, "Response body"); + } else { + assert_equals(res_body, uuid, "Response body"); + } + }, function(reason) { + if ("expected_type" in config && config.expected_type === "error") { + assert_throws(new TypeError(), function() { throw reason; }); + } else { + throw reason; + } + }); + }, + pause_after: "pause_after" in requests[i] && true || false + }); + } + + function pause() { + return new Promise(function(resolve, reject) { + step_timeout(function() { + return resolve() + }, 3000); + }); + } + + // TODO: it would be nice if this weren't serialised. + var idx = 0; + function run_next_step() { + if (fetch_functions.length) { + var fetch_function = fetch_functions.shift(); + if (fetch_function.pause_after > 0) { + return fetch_function.code(idx++) + .then(pause) + .then(run_next_step); + } else { + return fetch_function.code(idx++) + .then(run_next_step); + } + } else { + return Promise.resolve(); + } + } + + return run_next_step() + .then(function() { + // Now, query the server state + return server_state(uuid); + }).then(function(state) { + for (var i = 0; i < requests.length; ++i) { + var expected_validating_headers = [] + var req_num = i + 1; + if ("expected_type" in requests[i]) { + if (requests[i].expected_type === "cached") { + assert_true(state.length <= i, "cached response used for request " + req_num); + continue; // the server will not see the request, so we can't check anything else. + } + if (requests[i].expected_type === "not_cached") { + assert_false(state.length <= i, "cached response used for request " + req_num); + } + if (requests[i].expected_type === "etag_validated") { + expected_validating_headers.push('if-none-match') + } + if (requests[i].expected_type === "lm_validated") { + expected_validating_headers.push('if-modified-since') + } + } + for (var j in expected_validating_headers) { + var vhdr = expected_validating_headers[j]; + assert_own_property(state[i].request_headers, vhdr, " has " + vhdr + " request header"); + } + if ("expected_request_headers" in requests[i]) { + var expected_request_headers = requests[i].expected_request_headers; + for (var j = 0; j < expected_request_headers.length; ++j) { + var expected_header = expected_request_headers[j]; + assert_equals(state[i].request_headers[expected_header[0].toLowerCase()], + expected_header[1]); + } + } + } + }); + }; +} + + +function run_tests(tests) +{ + tests.forEach(function(info) { + promise_test(make_test(info.requests), info.name); + }); +} + +function http_date(delta) { + return new Date(Date.now() + (delta * 1000)).toGMTString(); +} + +var content_store = {}; +function http_content(cs_key) { + if (cs_key in content_store) { + return content_store[cs_key]; + } else { + var content = btoa(Math.random() * Date.now()); + content_store[cs_key] = content; + return content; + } +} \ No newline at end of file diff --git a/tests/wpt/web-platform-tests/fetch/http-cache/cc-request.html b/tests/wpt/web-platform-tests/fetch/http-cache/cc-request.html index 05d6f6b8c098..6ea8fbc92fd7 100644 --- a/tests/wpt/web-platform-tests/fetch/http-cache/cc-request.html +++ b/tests/wpt/web-platform-tests/fetch/http-cache/cc-request.html @@ -201,7 +201,8 @@ request_headers: [ ["Cache-Control", "only-if-cached"] ], - expected_status: 504 + expected_status: 504, + expected_response_text: "" } ] } diff --git a/tests/wpt/web-platform-tests/fetch/http-cache/heuristic.html b/tests/wpt/web-platform-tests/fetch/http-cache/heuristic.html index 429dddace6af..81deb1d06884 100644 --- a/tests/wpt/web-platform-tests/fetch/http-cache/heuristic.html +++ b/tests/wpt/web-platform-tests/fetch/http-cache/heuristic.html @@ -26,6 +26,7 @@ }, { expected_type: "cached", + response_status: [299, "Whatever"], } ] }, @@ -35,8 +36,7 @@ { response_status: [299, "Whatever"], response_headers: [ - ['Last-Modified', http_date(-3 * 100)], - ['Cache-Control', 'public'] + ['Last-Modified', http_date(-3 * 100)] ], }, { diff --git a/tests/wpt/web-platform-tests/fetch/http-cache/partial.html b/tests/wpt/web-platform-tests/fetch/http-cache/partial.html index 3ad593375ac6..8d5d61d46c44 100644 --- a/tests/wpt/web-platform-tests/fetch/http-cache/partial.html +++ b/tests/wpt/web-platform-tests/fetch/http-cache/partial.html @@ -24,7 +24,7 @@ response_status: [206, "Partial Content"], response_headers: [ ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 0-4/10'] + ['Content-Range', 'bytes 4-9/10'] ], response_body: "01234", expected_request_headers: [ @@ -36,12 +36,13 @@ ['Range', "bytes=-5"] ], expected_type: "cached", - expected_status: 206 + expected_status: 206, + expected_response_text: "01234" } ] }, { - name: 'HTTP cache stores complete response and serves smaller ranges from it.', + name: 'HTTP cache stores complete response and serves smaller ranges from it(byte-range-spec).', requests: [ { response_headers: [ @@ -51,15 +52,54 @@ }, { request_headers: [ - ['Range', "bytes=-1"] + ['Range', "bytes=0-1"] ], expected_type: "cached", + expected_status: 206, expected_response_text: "01" + }, + ] + }, + { + name: 'HTTP cache stores complete response and serves smaller ranges from it(absent last-byte-pos).', + requests: [ + { + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ], + response_body: "01234567890", + }, + { + request_headers: [ + ['Range', "bytes=1-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "1234567890" + } + ] + }, + { + name: 'HTTP cache stores complete response and serves smaller ranges from it(suffix-byte-range-spec).', + requests: [ + { + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ], + response_body: "0123456789A", + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "A" } ] }, { - name: 'HTTP cache stores partial response and serves smaller ranges from it.', + name: 'HTTP cache stores partial response and serves smaller ranges from it(byte-range-spec).', requests: [ { request_headers: [ @@ -68,7 +108,55 @@ response_status: [206, "Partial Content"], response_headers: [ ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 0-4/10'] + ['Content-Range', 'bytes 4-9/10'] + ], + response_body: "01234", + }, + { + request_headers: [ + ['Range', "bytes=6-8"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: 'HTTP cache stores partial response and serves smaller ranges from it(absent last-byte-pos).', + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ['Content-Range', 'bytes 4-9/10'] + ], + response_body: "01234", + }, + { + request_headers: [ + ['Range', "bytes=6-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: 'HTTP cache stores partial response and serves smaller ranges from it(suffix-byte-range-spec).', + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ['Content-Range', 'bytes 4-9/10'] ], response_body: "01234", }, @@ -77,7 +165,8 @@ ['Range', "bytes=-1"] ], expected_type: "cached", - expected_response_text: "01" + expected_status: 206, + expected_response_text: "4" } ] }, diff --git a/tests/wpt/web-platform-tests/fetch/http-cache/vary.html b/tests/wpt/web-platform-tests/fetch/http-cache/vary.html index 2f4b945b0af5..dd42b14f27ad 100644 --- a/tests/wpt/web-platform-tests/fetch/http-cache/vary.html +++ b/tests/wpt/web-platform-tests/fetch/http-cache/vary.html @@ -103,6 +103,7 @@ request_headers: [ ["Foo", "1"] ], + response_body: http_content('foo_1'), expected_type: "cached" } ] @@ -245,11 +246,12 @@ ] }, { - name: "HTTP cache doesn't use three-way Vary response when request omits variant header.", + name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order.", requests: [ { request_headers: [ ["Foo", "1"], + ["Bar", "abc4"], ["Baz", "789"] ], response_headers: [ @@ -259,6 +261,57 @@ ] }, { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header.", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", http_date(5000)], + ["Last-Modified", http_date(-3000)], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response with a field value of '*'.", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", http_date(5000)], + ["Last-Modified", http_date(-3000)], + ["Vary", "*"] + ] + }, + { + request_headers: [ + ["*", "1"], + ["Baz", "789"] + ], expected_type: "not_cached" } ]