Skip to main content

net/
http_loader.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashSet;
6use std::iter::FromIterator;
7use std::sync::Arc as StdArc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::{Duration, SystemTime};
10
11use async_recursion::async_recursion;
12use content_security_policy::percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
13use embedder_traits::{AuthenticationResponse, GenericEmbedderProxy};
14use futures::{TryFutureExt, TryStreamExt, future};
15use headers::authorization::Basic;
16use headers::{
17    AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
18    AccessControlMaxAge, AccessControlRequestMethod, Authorization, CacheControl, ContentLength,
19    HeaderMapExt, IfModifiedSince, LastModified, Pragma, Referer, StrictTransportSecurity,
20    UserAgent,
21};
22use http::header::{
23    self, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION,
24    CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE,
25    WWW_AUTHENTICATE,
26};
27use http::{HeaderMap, Method, Request as HyperRequest, StatusCode};
28use http_body_util::combinators::BoxBody;
29use http_body_util::{BodyExt, Full};
30use hyper::Response as HyperResponse;
31use hyper::body::{Bytes, Frame};
32use hyper::ext::ReasonPhrase;
33use hyper::header::{HeaderName, TRANSFER_ENCODING};
34use ipc_channel::IpcError;
35use ipc_channel::ipc::{self, IpcSender};
36use ipc_channel::router::ROUTER;
37use log::{debug, error, info, log_enabled, warn};
38use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
39use net_traits::blob_url_store::UrlWithBlobClaim;
40use net_traits::fetch::headers::get_value_from_header_list;
41use net_traits::http_status::HttpStatus;
42use net_traits::policy_container::{EmbedderPolicyValue, RequestPolicyContainer};
43use net_traits::pub_domains::{is_same_site, reg_suffix};
44use net_traits::request::{
45    BodyChunkRequest, BodyChunkResponse, CacheMode, CredentialsMode, Destination, Initiator,
46    Origin, RedirectMode, Referrer, Request, RequestBuilder, RequestClient, RequestMode,
47    ResponseTainting, ServiceWorkersMode, TraversableForUserPrompts, get_cors_unsafe_header_names,
48    is_cors_non_wildcard_request_header_name, is_cors_safelisted_method,
49    is_cors_safelisted_request_header,
50};
51use net_traits::response::{CacheState, RedirectTaint, Response, ResponseBody, ResponseType};
52use net_traits::{
53    CookieSource, DOCUMENT_ACCEPT_HEADER_VALUE, DiscardFetch, NetworkError, RedirectEndValue,
54    RedirectStartValue, ReferrerPolicy, ResourceAttribute, ResourceFetchTiming,
55    ResourceFetchTimingContainer, ResourceTimeValue, ResourceTimingType, ServoCipherSuite,
56    ServoNamedGroup, ServoProtocolVersion, TlsSecurityInfo, TlsSecurityState,
57};
58use parking_lot::{Mutex, RwLock};
59use profile_traits::mem::{Report, ReportKind};
60use profile_traits::path;
61#[cfg(feature = "tracing")]
62use profile_traits::trace_span;
63use rustc_hash::FxHashMap;
64use servo_base::cross_process_instant::CrossProcessInstant;
65use servo_base::generic_channel::GenericSharedMemory;
66use servo_base::id::HistoryStateId;
67use servo_config::pref;
68use servo_url::{ImmutableOrigin, ServoUrl};
69use tokio::sync::mpsc::{
70    Receiver as TokioReceiver, Sender as TokioSender, UnboundedReceiver, UnboundedSender, channel,
71    unbounded_channel,
72};
73use tokio_stream::wrappers::ReceiverStream;
74#[cfg(feature = "tracing")]
75use tracing::Instrument;
76use uuid::Uuid;
77
78use crate::async_runtime::spawn_task;
79use crate::connector::{
80    CertificateErrorOverrideManager, ServoClient, TlsHandshakeInfo, create_tls_config,
81};
82use crate::cookie::ServoCookie;
83use crate::cookie_storage::CookieStorage;
84use crate::decoder::Decoder;
85#[cfg(feature = "devtools")]
86use crate::devtools::{
87    prepare_devtools_request, send_request_to_devtools, send_response_values_to_devtools,
88};
89use crate::devtools::{
90    send_early_httprequest_to_devtools, send_response_to_devtools, send_security_info_to_devtools,
91};
92use crate::embedder::NetToEmbedderMsg;
93use crate::fetch::cors_cache::CorsCache;
94use crate::fetch::fetch_params::FetchParams;
95use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser};
96use crate::fetch::methods::{Data, DoneChannel, FetchContext, Target, fetch, main_fetch};
97use crate::hsts::HstsList;
98use crate::http_cache::{
99    CacheKey, CachedResourcesOrGuard, HttpCache, ValidationStatus, construct_response,
100    invalidate_cached_resources, refresh,
101};
102use crate::resource_thread::{AuthCache, AuthCacheEntry};
103use crate::websocket_loader::start_websocket;
104
105#[cfg(feature = "devtools")]
106type DevtoolsResponse = Option<devtools_traits::ChromeToDevtoolsControlMsg>;
107
108#[cfg(not(feature = "devtools"))]
109type DevtoolsResponse = Option<()>;
110
111/// The various states an entry of the HttpCache can be in.
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub enum HttpCacheEntryState {
114    /// The entry is fully up-to-date,
115    /// there are no pending concurrent stores,
116    /// and it is ready to construct cached responses.
117    ReadyToConstruct,
118    /// The entry is pending a number of concurrent stores.
119    PendingStore(usize),
120}
121
122pub struct HttpState {
123    pub hsts_list: RwLock<HstsList>,
124    pub cookie_jar: RwLock<CookieStorage>,
125    pub http_cache: HttpCache,
126    pub auth_cache: RwLock<AuthCache>,
127    pub cors_cache: RwLock<CorsCache>,
128    pub history_states: RwLock<FxHashMap<HistoryStateId, Vec<u8>>>,
129    pub client: ServoClient,
130    pub override_manager: CertificateErrorOverrideManager,
131    pub embedder_proxy: GenericEmbedderProxy<NetToEmbedderMsg>,
132}
133
134impl HttpState {
135    pub(crate) fn memory_reports(&self, suffix: &str, ops: &mut MallocSizeOfOps) -> Vec<Report> {
136        vec![
137            Report {
138                path: path!["memory-cache", suffix],
139                kind: ReportKind::ExplicitJemallocHeapSize,
140                size: self.http_cache.size_of(ops),
141            },
142            Report {
143                path: path!["hsts-list", suffix],
144                kind: ReportKind::ExplicitJemallocHeapSize,
145                size: self.hsts_list.read().size_of(ops),
146            },
147            Report {
148                path: path!["auth cache", suffix],
149                kind: ReportKind::ExplicitJemallocHeapSize,
150                size: self.auth_cache.read().size_of(ops),
151            },
152            Report {
153                path: path!["cookie storage", suffix],
154                kind: ReportKind::ExplicitJemallocHeapSize,
155                size: self.cookie_jar.read().size_of(ops),
156            },
157        ]
158    }
159
160    async fn request_authentication(
161        &self,
162        request: &Request,
163        response: &Response,
164    ) -> Option<AuthenticationResponse> {
165        // We do not make an authentication request for non-WebView associated HTTP requests.
166        let webview_id = request.target_webview_id?;
167        let for_proxy = response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED;
168
169        // If this is not actually a navigation request return None.
170        if request.mode != RequestMode::Navigate {
171            return None;
172        }
173
174        let (sender, receiver) = tokio::sync::oneshot::channel();
175        self.embedder_proxy
176            .send(NetToEmbedderMsg::RequestAuthentication(
177                webview_id,
178                request.url(),
179                for_proxy,
180                sender,
181            ));
182        receiver.await.ok()?
183    }
184}
185
186/// Step 11 of <https://fetch.spec.whatwg.org/#concept-fetch>.
187pub(crate) fn set_default_accept(request: &mut Request) {
188    // Step 11. If request’s header list does not contain `Accept`, then:
189    if request.headers.contains_key(header::ACCEPT) {
190        return;
191    }
192
193    // Step 11.2. If request’s initiator is "prefetch", then set value to the document `Accept` header value.
194    let value = if request.initiator == Initiator::Prefetch {
195        DOCUMENT_ACCEPT_HEADER_VALUE
196    } else {
197        // Step 11.3. Otherwise, the user agent should set value to the first matching statement,
198        // if any, switching on request’s destination:
199        match request.destination {
200            Destination::Document | Destination::Frame | Destination::IFrame => {
201                DOCUMENT_ACCEPT_HEADER_VALUE
202            },
203            Destination::Image => {
204                HeaderValue::from_static("image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5")
205            },
206            Destination::Json => HeaderValue::from_static("application/json,*/*;q=0.5"),
207            Destination::Style => HeaderValue::from_static("text/css,*/*;q=0.1"),
208            Destination::Text => HeaderValue::from_static("text/plain,*/*;q=0.5"),
209            // Step 11.1. Let value be `*/*`.
210            _ => HeaderValue::from_static("*/*"),
211        }
212    };
213
214    // Step 11.4. Append (`Accept`, value) to request’s header list.
215    request.headers.insert(header::ACCEPT, value);
216}
217
218fn set_default_accept_encoding(headers: &mut HeaderMap) {
219    if headers.contains_key(header::ACCEPT_ENCODING) {
220        return;
221    }
222
223    // TODO(eijebong): Change this once typed headers are done
224    headers.insert(
225        header::ACCEPT_ENCODING,
226        HeaderValue::from_static("gzip, deflate, br, zstd"),
227    );
228}
229
230/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-state-no-referrer-when-downgrade>
231fn no_referrer_when_downgrade(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
232    // Step 1
233    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
234        return None;
235    }
236    // Step 2
237    strip_url_for_use_as_referrer(referrer_url, false)
238}
239
240/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin>
241fn strict_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
242    // Step 1
243    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
244        return None;
245    }
246    // Step 2
247    strip_url_for_use_as_referrer(referrer_url, true)
248}
249
250/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin-when-cross-origin>
251fn strict_origin_when_cross_origin(
252    referrer_url: ServoUrl,
253    current_url: ServoUrl,
254) -> Option<ServoUrl> {
255    // Step 1
256    if referrer_url.origin() == current_url.origin() {
257        return strip_url_for_use_as_referrer(referrer_url, false);
258    }
259    // Step 2
260    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
261        return None;
262    }
263    // Step 3
264    strip_url_for_use_as_referrer(referrer_url, true)
265}
266
267/// <https://html.spec.whatwg.org/multipage/#schemelessly-same-site>
268fn is_schemelessy_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
269    // Step 1
270    if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
271        true
272    } else if site_a.is_tuple() && site_b.is_tuple() {
273        // Step 2.1
274        let host_a = site_a.host().map(|h| h.to_string()).unwrap_or_default();
275        let host_b = site_b.host().map(|h| h.to_string()).unwrap_or_default();
276
277        let host_a_reg = reg_suffix(&host_a);
278        let host_b_reg = reg_suffix(&host_b);
279
280        // Step 2.2-2.3
281        (site_a.host() == site_b.host() && host_a_reg.is_empty()) ||
282            (host_a_reg == host_b_reg && !host_a_reg.is_empty())
283    } else {
284        // Step 3
285        false
286    }
287}
288
289/// <https://w3c.github.io/webappsec-referrer-policy/#strip-url>
290fn strip_url_for_use_as_referrer(mut url: ServoUrl, origin_only: bool) -> Option<ServoUrl> {
291    const MAX_REFERRER_URL_LENGTH: usize = 4096;
292    // Step 2
293    if url.is_local_scheme() {
294        return None;
295    }
296    // Step 3-6
297    {
298        let url = url.as_mut_url();
299        let _ = url.set_username("");
300        let _ = url.set_password(None);
301        url.set_fragment(None);
302        // Note: The result of serializing referrer url should not be
303        // greater than 4096 as specified in Step 6 of
304        // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
305        if origin_only || url.as_str().len() > MAX_REFERRER_URL_LENGTH {
306            url.set_path("");
307            url.set_query(None);
308        }
309    }
310    // Step 7
311    Some(url)
312}
313
314/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-same-origin>
315fn same_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
316    // Step 1
317    if referrer_url.origin() == current_url.origin() {
318        return strip_url_for_use_as_referrer(referrer_url, false);
319    }
320    // Step 2
321    None
322}
323
324/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-origin-when-cross-origin>
325fn origin_when_cross_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
326    // Step 1
327    if referrer_url.origin() == current_url.origin() {
328        return strip_url_for_use_as_referrer(referrer_url, false);
329    }
330    // Step 2
331    strip_url_for_use_as_referrer(referrer_url, true)
332}
333
334/// <https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer>
335pub fn determine_requests_referrer(
336    referrer_policy: ReferrerPolicy,
337    referrer_source: ServoUrl,
338    current_url: ServoUrl,
339) -> Option<ServoUrl> {
340    match referrer_policy {
341        ReferrerPolicy::EmptyString | ReferrerPolicy::NoReferrer => None,
342        ReferrerPolicy::Origin => strip_url_for_use_as_referrer(referrer_source, true),
343        ReferrerPolicy::UnsafeUrl => strip_url_for_use_as_referrer(referrer_source, false),
344        ReferrerPolicy::StrictOrigin => strict_origin(referrer_source, current_url),
345        ReferrerPolicy::StrictOriginWhenCrossOrigin => {
346            strict_origin_when_cross_origin(referrer_source, current_url)
347        },
348        ReferrerPolicy::SameOrigin => same_origin(referrer_source, current_url),
349        ReferrerPolicy::OriginWhenCrossOrigin => {
350            origin_when_cross_origin(referrer_source, current_url)
351        },
352        ReferrerPolicy::NoReferrerWhenDowngrade => {
353            no_referrer_when_downgrade(referrer_source, current_url)
354        },
355    }
356}
357
358fn set_request_cookies(
359    url: &ServoUrl,
360    headers: &mut HeaderMap,
361    cookie_jar: &RwLock<CookieStorage>,
362) {
363    let mut cookie_jar = cookie_jar.write();
364    cookie_jar.remove_expired_cookies_for_url(url);
365    if let Some(cookie_list) = cookie_jar.cookies_for_url(url, CookieSource::HTTP) &&
366        let Ok(cookie_list_header_value) = HeaderValue::from_bytes(cookie_list.as_bytes())
367    {
368        headers.insert(header::COOKIE, cookie_list_header_value);
369    }
370}
371
372fn set_cookie_for_url(cookie_jar: &RwLock<CookieStorage>, request: &ServoUrl, cookie_val: &str) {
373    let mut cookie_jar = cookie_jar.write();
374    let source = CookieSource::HTTP;
375
376    if let Some(cookie) = ServoCookie::from_cookie_string(cookie_val, request, source) {
377        cookie_jar.push(cookie, request, source);
378    }
379}
380
381fn set_cookies_from_headers(
382    url: &ServoUrl,
383    headers: &HeaderMap,
384    cookie_jar: &RwLock<CookieStorage>,
385) {
386    for cookie in headers.get_all(header::SET_COOKIE) {
387        let cookie_bytes = cookie.as_bytes();
388        if !ServoCookie::is_valid_name_or_value(cookie_bytes) {
389            continue;
390        }
391        if let Ok(cookie_str) = std::str::from_utf8(cookie_bytes) {
392            set_cookie_for_url(cookie_jar, url, cookie_str);
393        }
394    }
395}
396
397fn build_tls_security_info(handshake: &TlsHandshakeInfo, hsts_enabled: bool) -> TlsSecurityInfo {
398    // Simplified security state determination:
399    // Servo uses rustls, which only supports TLS 1.2+ and secure cipher suites (GCM, ChaCha20-Poly1305).
400    // rustls does NOT support TLS 1.0, TLS 1.1, SSL, or weak ciphers (RC4, 3DES, CBC, etc).
401    // Therefore, any successful TLS connection is secure by design.
402    //
403    // We only check for missing handshake information as a defensive measure.
404
405    let state = if handshake.protocol_version.is_none() || handshake.cipher_suite.is_none() {
406        // Missing handshake information indicates an incomplete or failed connection
407        TlsSecurityState::Insecure
408    } else {
409        // rustls guarantees TLS 1.2+ with secure ciphers
410        TlsSecurityState::Secure
411    };
412
413    TlsSecurityInfo {
414        state,
415        weakness_reasons: Vec::new(), // rustls never negotiates weak crypto
416        protocol_version: handshake.protocol_version.map(ServoProtocolVersion),
417        cipher_suite: handshake.cipher_suite.map(ServoCipherSuite),
418        kea_group_name: handshake.kea_group_name.map(ServoNamedGroup),
419        signature_scheme_name: handshake.signature_scheme_name.clone(),
420        alpn_protocol: handshake.alpn_protocol.clone(),
421        certificate_chain_der: handshake.certificate_chain_der.clone(),
422        certificate_transparency: None,
423        hsts: hsts_enabled,
424        hpkp: false,
425        used_ech: handshake.used_ech,
426        used_delegated_credentials: false,
427        used_ocsp: false,
428        used_private_dns: false,
429    }
430}
431
432fn auth_from_cache(
433    auth_cache: &RwLock<AuthCache>,
434    origin: &ImmutableOrigin,
435) -> Option<Authorization<Basic>> {
436    if let Some(auth_entry) = auth_cache
437        .read()
438        .entries
439        .get(origin.ascii_serialization().as_ref())
440    {
441        let user_name = &auth_entry.user_name;
442        let password = &auth_entry.password;
443        Some(Authorization::basic(user_name, password))
444    } else {
445        None
446    }
447}
448
449/// Messages from the IPC route to the fetch worker,
450/// used to fill the body with bytes coming-in over IPC.
451enum BodyChunk {
452    /// A chunk of bytes.
453    Chunk(GenericSharedMemory),
454    /// Body is done.
455    Done,
456}
457
458/// The stream side of the body passed to hyper.
459enum BodyStream {
460    /// A receiver that can be used in Body::wrap_stream,
461    /// for streaming the request over the network.
462    Chunked(TokioReceiver<Result<Frame<Bytes>, hyper::Error>>),
463    /// A body whose bytes are buffered
464    /// and sent in one chunk over the network.
465    Buffered(UnboundedReceiver<BodyChunk>),
466}
467
468/// The sink side of the body passed to hyper,
469/// used to enqueue chunks.
470enum BodySink {
471    /// A Tokio sender used to feed chunks to the network stream.
472    Chunked(TokioSender<Result<Frame<Bytes>, hyper::Error>>),
473    /// A Crossbeam sender used to send chunks to the fetch worker,
474    /// where they will be buffered
475    /// in order to ensure they are not streamed them over the network.
476    Buffered(UnboundedSender<BodyChunk>),
477}
478
479impl BodySink {
480    fn transmit_bytes(&self, bytes: GenericSharedMemory) {
481        match self {
482            BodySink::Chunked(sender) => {
483                let sender = sender.clone();
484                spawn_task(async move {
485                    let _ = sender
486                        .send(Ok(Frame::data(Bytes::copy_from_slice(&bytes))))
487                        .await;
488                });
489            },
490            BodySink::Buffered(sender) => {
491                let _ = sender.send(BodyChunk::Chunk(bytes));
492            },
493        }
494    }
495
496    fn close(self) {
497        match self {
498            BodySink::Chunked(_) => {},
499            BodySink::Buffered(sender) => {
500                let _ = sender.send(BodyChunk::Done);
501            },
502        }
503    }
504}
505
506fn request_body_stream_closed_error(action: &str) -> NetworkError {
507    NetworkError::Crash(format!(
508        "Request body stream has already been closed while trying to {action}."
509    ))
510}
511
512fn log_request_body_stream_closed(action: &str, error: Option<&IpcError>) {
513    match error {
514        Some(error) => {
515            error!("Request body stream has already been closed while trying to {action}: {error}")
516        },
517        None => error!("Request body stream has already been closed while trying to {action}."),
518    }
519}
520
521fn log_fetch_terminated_send_failure(terminated_with_error: bool, context: &str) {
522    warn!(
523        "Failed to notify request-body stream termination state ({terminated_with_error}) while {context} because the receiver was already dropped."
524    );
525}
526
527const FRAGMENT: &AsciiSet = &CONTROLS.add(b'|').add(b'{').add(b'}');
528
529#[servo_tracing::instrument(skip_all, fields(url=url.as_str()))]
530/// This sets up the callback infrastructure to send body frames to `body_sender` and fires the client request.
531async fn obtain_response(
532    fetch_params: &mut FetchParams,
533    client: &ServoClient,
534    url: &ServoUrl,
535    body_sender: Option<StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>>,
536    request_id: Option<&Uuid>,
537    context: &FetchContext,
538    fetch_terminated: UnboundedSender<bool>,
539) -> Result<(HyperResponse<Decoder>, DevtoolsResponse), NetworkError> {
540    #[cfg(feature = "devtools")]
541    let (destination, pipeline_id, browsing_context_id) = {
542        let request = &fetch_params.request;
543
544        (
545            request.destination,
546            request.pipeline_id,
547            request.target_webview_id.map(Into::into),
548        )
549    };
550
551    let method = &fetch_params.request.method;
552    let source_is_null = fetch_params
553        .request
554        .body
555        .as_ref()
556        .is_some_and(|body| body.source_is_null());
557    let headers = &mut fetch_params.request.headers;
558
559    #[cfg(feature = "devtools")]
560    let devtools_bytes = StdArc::new(Mutex::new(vec![]));
561
562    // https://url.spec.whatwg.org/#percent-encoded-bytes
563    let encoded_url = utf8_percent_encode(url.as_str(), FRAGMENT).to_string();
564
565    let request = if let Some(chunk_requester) = body_sender {
566        let (sink, stream) = if source_is_null {
567            // Step 4.2 of https://fetch.spec.whatwg.org/#concept-http-network-fetch
568            // TODO: this should not be set for HTTP/2(currently not supported?).
569            headers.insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
570
571            let (sender, receiver) = channel(1);
572            (BodySink::Chunked(sender), BodyStream::Chunked(receiver))
573        } else {
574            // Note: Hyper seems to already buffer bytes when the request appears not stream-able,
575            // see https://github.com/hyperium/hyper/issues/2232#issuecomment-644322104
576            //
577            // However since this doesn't appear documented, and we're using an ancient version,
578            // for now we buffer manually to ensure we don't stream requests
579            // to servers that might not know how to handle them.
580            let (sender, receiver) = unbounded_channel();
581            (BodySink::Buffered(sender), BodyStream::Buffered(receiver))
582        };
583
584        #[cfg(feature = "devtools")]
585        obtain_response_setup_router_callback(
586            Some(devtools_bytes.clone()),
587            chunk_requester,
588            sink,
589            fetch_terminated,
590        )?;
591
592        #[cfg(not(feature = "devtools"))]
593        obtain_response_setup_router_callback(None, chunk_requester, sink, fetch_terminated)?;
594
595        let body = match stream {
596            BodyStream::Chunked(receiver) => {
597                let stream = ReceiverStream::new(receiver);
598                BoxBody::new(http_body_util::StreamBody::new(stream))
599            },
600            BodyStream::Buffered(mut receiver) => {
601                // Accumulate bytes received over IPC into a vector.
602                let mut body = vec![];
603                loop {
604                    match receiver.recv().await {
605                        Some(BodyChunk::Chunk(bytes)) => {
606                            body.extend_from_slice(&bytes);
607                        },
608                        Some(BodyChunk::Done) => break,
609                        None => warn!("Failed to read all chunks from request body."),
610                    }
611                }
612                Full::new(body.into()).map_err(|_| unreachable!()).boxed()
613            },
614        };
615        HyperRequest::builder()
616            .method(method)
617            .uri(encoded_url)
618            .body(body)
619    } else {
620        HyperRequest::builder()
621            .method(method)
622            .uri(encoded_url)
623            .body(
624                http_body_util::Empty::new()
625                    .map_err(|_| unreachable!())
626                    .boxed(),
627            )
628    };
629
630    // TODO(#21261) connect_start: set if a persistent connection is *not* used and the last non-redirected
631    // fetch passes the timing allow check
632    let connect_start = CrossProcessInstant::now();
633    fetch_params.timing_info.set_attributes(&[
634        ResourceAttribute::DomainLookupStart,
635        ResourceAttribute::ConnectStart(connect_start),
636    ]);
637
638    // TODO: We currently don't know when the handhhake before the connection is done
639    // so our best bet would be to set `secure_connection_start` here when we are currently
640    // fetching on a HTTPS url.
641    if url.scheme() == "https" {
642        fetch_params
643            .timing_info
644            .set_attribute(ResourceAttribute::SecureConnectionStart);
645    }
646
647    let mut request = match request {
648        Ok(request) => request,
649        Err(error) => return Err(NetworkError::HttpError(error.to_string())),
650    };
651    *request.headers_mut() = headers.clone();
652
653    let connect_end = CrossProcessInstant::now();
654    fetch_params
655        .timing_info
656        .set_attribute(ResourceAttribute::ConnectEnd(connect_end));
657
658    let host = request.uri().host().unwrap_or("").to_owned();
659    let override_manager = context.state.override_manager.clone();
660    #[cfg(feature = "devtools")]
661    let (request_id, pipeline_id, closure_url, method, send_start, headers) = (
662        request_id.map(|value| value.to_owned()),
663        pipeline_id,
664        url.clone(),
665        method.clone(),
666        CrossProcessInstant::now(),
667        headers.clone(),
668    );
669    let is_secure_scheme = url.is_secure_scheme();
670
671    // Generally, we use a persistent connection, so we will also set other PerformanceResourceTiming
672    //   attributes to this as well (domain_lookup_start, domain_lookup_end, connect_start, connect_end,
673    //   secure_connection_start)
674    fetch_params
675        .timing_info
676        .set_attribute(ResourceAttribute::RequestStart);
677
678    let client_future = client
679        .request(request)
680        .and_then(move |res| {
681            // TODO(#21271) response_start: immediately after receiving first byte of response
682            #[cfg(feature = "devtools")]
683            let msg = if let Some(request_id) = request_id {
684                let send_end = CrossProcessInstant::now();
685                if let Some(pipeline_id) = pipeline_id {
686                    if let Some(browsing_context_id) = browsing_context_id {
687                        // XHR uses the default destination;
688                        // other kinds of fetches (which haven't been implemented yet) do not.
689                        // Once we support other kinds of fetches we'll need to be more fine grained here
690                        // since things like image fetches are classified differently by devtools
691                        let is_xhr = destination == Destination::None;
692
693                        Some(prepare_devtools_request(
694                            request_id,
695                            closure_url,
696                            method.clone(),
697                            headers,
698                            Some(devtools_bytes.lock().clone()),
699                            pipeline_id,
700                            (connect_end - connect_start).unsigned_abs(),
701                            (send_end - send_start).unsigned_abs(),
702                            destination,
703                            is_xhr,
704                            browsing_context_id,
705                        ))
706                    } else {
707                        debug!("Not notifying devtools (no browsing_context_id)");
708                        None
709                    }
710                    // TODO: ^This is not right, connect_start is taken before contructing the
711                    // request and connect_end at the end of it. send_start is takend before the
712                    // connection too. I'm not sure it's currently possible to get the time at the
713                    // point between the connection and the start of a request.
714                } else {
715                    debug!("Not notifying devtools (no pipeline_id)");
716                    None
717                }
718            } else {
719                debug!("Not notifying devtools (no request_id)");
720                None
721            };
722
723            future::ready(Ok((
724                Decoder::detect(res.map(|r| r.boxed()), is_secure_scheme),
725                #[cfg(feature = "devtools")]
726                msg,
727                #[cfg(not(feature = "devtools"))]
728                None,
729            )))
730        })
731        .map_err(move |error| {
732            warn!("network error: {error:?}");
733            NetworkError::from_hyper_error(
734                &error,
735                override_manager.remove_certificate_failing_verification(host.as_str()),
736            )
737        });
738
739    #[cfg(feature = "tracing")]
740    {
741        client_future.instrument(trace_span!("HyperRequest")).await
742    }
743
744    #[cfg(not(feature = "tracing"))]
745    {
746        client_future.await
747    }
748}
749
750/// Setup the callback mechanism to forward chunks from the request received to the `chunk_requester`.
751fn obtain_response_setup_router_callback(
752    devtools_bytes: Option<StdArc<Mutex<Vec<u8>>>>,
753    chunk_requester: StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>,
754    sink: BodySink,
755    fetch_terminated: UnboundedSender<bool>,
756) -> Result<(), NetworkError> {
757    let (body_chan, body_port) = ipc::channel().unwrap();
758
759    {
760        let mut lock = chunk_requester.lock();
761        if let Some(chunk_requester) = lock.as_mut() {
762            if let Err(error) = chunk_requester.send(BodyChunkRequest::Connect(body_chan)) {
763                log_request_body_stream_closed("connect to the request body stream", Some(&error));
764                return Err(request_body_stream_closed_error(
765                    "connect to the request body stream",
766                ));
767            }
768
769            // https://fetch.spec.whatwg.org/#concept-request-transmit-body
770            // Request the first chunk, corresponding to Step 3 and 4.
771            if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
772                log_request_body_stream_closed(
773                    "request the first request body chunk",
774                    Some(&error),
775                );
776                return Err(request_body_stream_closed_error(
777                    "request the first request body chunk",
778                ));
779            }
780        } else {
781            log_request_body_stream_closed("connect to the request body stream", None);
782            return Err(request_body_stream_closed_error(
783                "connect to the request body stream",
784            ));
785        }
786    }
787
788    let mut sink = Some(sink);
789
790    ROUTER.add_typed_route(
791        body_port,
792        Box::new(move |message| {
793            info!("Received message");
794            let bytes = match message.unwrap() {
795                BodyChunkResponse::Chunk(bytes) => bytes,
796                BodyChunkResponse::Done => {
797                    // Step 2.2.2. If fetchParams’s process request end-of-body is non-null,
798                    // then run fetchParams’s process request end-of-body.
799                    if fetch_terminated.send(false).is_err() {
800                        log_fetch_terminated_send_failure(
801                            false,
802                            "handling request body completion",
803                        );
804                    }
805                    if let Some(sink) = sink.take() {
806                        sink.close();
807                    }
808
809                    return;
810                },
811                BodyChunkResponse::Error => {
812                    // Step 4 and/or 5.
813                    // TODO: differentiate between the two steps,
814                    // where step 5 requires setting an `aborted` flag on the fetch.
815                    if fetch_terminated.send(true).is_err() {
816                        log_fetch_terminated_send_failure(
817                            true,
818                            "handling request body stream error",
819                        );
820                    }
821                    if let Some(sink) = sink.take() {
822                        sink.close();
823                    }
824
825                    return;
826                },
827            };
828
829            #[cfg(feature = "devtools")]
830            if let Some(devtools_bytes) = &devtools_bytes {
831                devtools_bytes.lock().extend_from_slice(&bytes);
832            }
833
834            // Step 5.1.2.2, transmit chunk over the network,
835            // currently implemented by sending the bytes to the fetch worker.
836            {
837                let Some(sink) = sink.as_ref() else {
838                    return;
839                };
840                sink.transmit_bytes(bytes);
841            }
842
843            // Step 5.1.2.3
844            // Request the next chunk.
845            let mut chunk_requester = chunk_requester.lock();
846            if let Some(chunk_requester) = chunk_requester.as_mut() {
847                if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
848                    log_request_body_stream_closed(
849                        "request the next request body chunk",
850                        Some(&error),
851                    );
852                    if fetch_terminated.send(true).is_err() {
853                        log_fetch_terminated_send_failure(
854                            true,
855                            "handling failure to request the next request body chunk",
856                        );
857                    }
858                    if let Some(sink) = sink.take() {
859                        sink.close();
860                    }
861                }
862            } else {
863                log_request_body_stream_closed("request the next request body chunk", None);
864                if fetch_terminated.send(true).is_err() {
865                    log_fetch_terminated_send_failure(
866                        true,
867                        "handling a closed request body stream while requesting the next chunk",
868                    );
869                }
870                if let Some(sink) = sink.take() {
871                    sink.close();
872                }
873            }
874        }),
875    );
876
877    Ok(())
878}
879
880/// [HTTP fetch](https://fetch.spec.whatwg.org/#concept-http-fetch)
881#[async_recursion]
882pub(crate) async fn http_fetch(
883    fetch_params: &mut FetchParams,
884    cors_flag: bool,
885    cors_preflight_flag: bool,
886    authentication_fetch_flag: bool,
887    target: Target<'async_recursion>,
888    done_chan: &mut DoneChannel,
889    context: &FetchContext,
890) -> Response {
891    // This is a new async fetch, reset the channel we are waiting on
892    *done_chan = None;
893    // Step 1. Let request be fetchParams’s request.
894    let request = &mut fetch_params.request;
895
896    // Step 2. Let response and internalResponse be null.
897    let mut response: Option<Response> = None;
898
899    // Step 3. If request’s service-workers mode is "all", then
900    if request.service_workers_mode == ServiceWorkersMode::All {
901        // TODO: Substep 1
902        // Set response to the result of invoking handle fetch for request.
903
904        // Substep 2
905        if let Some(ref res) = response {
906            // Subsubstep 1
907            // TODO: transmit body for request
908
909            // Subsubstep 2
910            // nothing to do, since actual_response is a function on response
911
912            // Subsubstep 3
913            if (res.response_type == ResponseType::Opaque && request.mode != RequestMode::NoCors) ||
914                (res.response_type == ResponseType::OpaqueRedirect &&
915                    request.redirect_mode != RedirectMode::Manual) ||
916                (res.url_list.len() > 1 && request.redirect_mode != RedirectMode::Follow) ||
917                res.is_network_error()
918            {
919                return Response::network_error(NetworkError::ConnectionFailure);
920            }
921
922            // Subsubstep 4
923            // TODO: set response's CSP list on actual_response
924        }
925    }
926
927    // Step 4. If response is null, then:
928    if response.is_none() {
929        // Step 4.1. If makeCORSPreflight is true and one of these conditions is true:
930        if cors_preflight_flag {
931            let (method_mismatch, header_mismatch) = {
932                let mut cors_cache = context.state.cors_cache.write();
933                let method_cache_match = cors_cache.match_method(request, &request.method);
934
935                // > There is no method cache entry match for request's method using request, and either
936                // > request's method is not a CORS-safelisted method or request's use-CORS-preflight flag
937                // > is set.
938                let method_mismatch = !method_cache_match &&
939                    (!is_cors_safelisted_method(&request.method) || request.use_cors_preflight);
940
941                // > There is at least one item in the CORS-unsafe request-header names with request's
942                // > header list for which there is no header-name cache entry match using request.
943                let header_mismatch = request.headers.iter().any(|(name, value)| {
944                    !cors_cache.match_header(request, name) &&
945                        !is_cors_safelisted_request_header(&name, &value)
946                });
947                (method_mismatch, header_mismatch)
948            };
949
950            // Then:
951            if method_mismatch || header_mismatch {
952                // Step 4.1.1. Let preflightResponse be the result of running
953                // CORS-preflight fetch given request.
954                let preflight_response = cors_preflight_fetch(request, context).await;
955                // Step 4.1.2. If preflightResponse is a network error, then return preflightResponse.
956                if let Some(error) = preflight_response.get_network_error() {
957                    return Response::network_error(error.clone());
958                }
959            }
960        }
961
962        // Step 4.2. If request’s redirect mode is "follow",
963        // then set request’s service-workers mode to "none".
964        if request.redirect_mode == RedirectMode::Follow {
965            request.service_workers_mode = ServiceWorkersMode::None;
966        }
967
968        // Step 4.3. Set response and internalResponse to the result of
969        // running HTTP-network-or-cache fetch given fetchParams.
970        let mut fetch_result = http_network_or_cache_fetch(
971            fetch_params,
972            authentication_fetch_flag,
973            cors_flag,
974            done_chan,
975            context,
976        )
977        .await;
978
979        // Step 4.4. If request’s response tainting is "cors" and a CORS check for request
980        // and response returns failure, then return a network error.
981        if cors_flag && cors_check(&fetch_params.request, &fetch_result).is_err() {
982            return Response::network_error(NetworkError::CorsGeneral);
983        }
984
985        // Step 4.5. If the TAO check for request and response returns failure,
986        // then set request’s timing allow failed flag.
987        if let Err(()) = tao_check(&fetch_params.request, &fetch_result) {
988            fetch_params.timing_info.inner().mark_timing_check_failed();
989        }
990        fetch_result.return_internal = false;
991        response = Some(fetch_result);
992    }
993
994    let request = &mut fetch_params.request;
995
996    // response is guaranteed to be something by now
997    let mut response = response.unwrap();
998
999    // Step 5: If either request’s response tainting or response’s type is "opaque",
1000    // and the cross-origin resource policy check with request’s origin, request’s client,
1001    // request’s destination, and internalResponse returns blocked, then return a network error.
1002    if (request.response_tainting == ResponseTainting::Opaque ||
1003        response.response_type == ResponseType::Opaque) &&
1004        request.client.as_ref().is_some_and(|client| {
1005            cross_origin_resource_policy_check(
1006                &request.origin,
1007                client,
1008                &response,
1009                ForNavigation::No,
1010            ) == CrossOriginResourcePolicy::Blocked
1011        })
1012    {
1013        return Response::network_error(NetworkError::CrossOriginResponse);
1014    }
1015
1016    // Step 6. If internalResponse’s status is a redirect status:
1017    if response
1018        .actual_response()
1019        .status
1020        .try_code()
1021        .is_some_and(is_redirect_status)
1022    {
1023        // TODO Step 6.1. If request is a navigation request, then append to a request’s navigation
1024        // timing allow values list given request and internalResponse.
1025
1026        // Step 6.2. If internalResponse’s status is not 303, request’s body is non-null,
1027        // and the connection uses HTTP/2, then user agents may, and are even encouraged to,
1028        // transmit an RST_STREAM frame.
1029        if response.actual_response().status != StatusCode::SEE_OTHER {
1030            // TODO: send RST_STREAM frame
1031        }
1032
1033        // Step 6.3. Switch on request’s redirect mode:
1034        response = match request.redirect_mode {
1035            // Step 6.3."error".1. Set response to a network error.
1036            RedirectMode::Error => Response::network_error(NetworkError::RedirectError),
1037            RedirectMode::Manual => {
1038                // Step 6.3."manual".1. If request’s mode is "navigate", then set fetchParams’s controller’s
1039                // next manual redirect steps to run HTTP-redirect fetch given fetchParams and response.
1040                if request.mode == RequestMode::Navigate {
1041                    // TODO: We don't implement Fetch controller. Instead, we update the location url
1042                    // of the response here and don't call `http_redirect_fetch`. That's get called later.
1043                    // Once we have a fetch controller here, we should update the code as specced.
1044                    let location_url =
1045                        location_url_for_response(&response, request.current_url().fragment());
1046                    response.actual_response_mut().location_url = location_url;
1047                    response
1048                } else {
1049                    // Step 6.3."manual".2. Otherwise, set response to an opaque-redirect filtered
1050                    // response whose internal response is internalResponse.
1051                    response.to_filtered(ResponseType::OpaqueRedirect)
1052                }
1053            },
1054            RedirectMode::Follow => {
1055                // TODO Step 6.3."follow".1. Run the WebDriver BiDi response completed steps with
1056                // request and response.
1057                // set back to default
1058                response.return_internal = true;
1059
1060                // Step 6.3."follow".2. Set response to the result of running HTTP-redirect fetch
1061                // given fetchParams and response.
1062                http_redirect_fetch(
1063                    fetch_params,
1064                    response,
1065                    cors_flag,
1066                    target,
1067                    done_chan,
1068                    context,
1069                )
1070                .await
1071            },
1072        };
1073    }
1074
1075    // set back to default
1076    response.return_internal = true;
1077    fetch_params
1078        .timing_info
1079        .set_attribute(ResourceAttribute::RedirectCount(
1080            fetch_params.request.redirect_count as u16,
1081        ));
1082
1083    response.resource_timing = fetch_params.timing_info.clone();
1084
1085    // Step 7. Return response
1086    response
1087}
1088
1089/// <https://fetch.spec.whatwg.org/#concept-tao-check>
1090fn tao_check(request: &Request, response: &Response) -> Result<(), ()> {
1091    // Step 1. Assert: request’s origin is not "client".
1092    let Origin::Origin(ref request_origin) = request.origin else {
1093        unreachable!("origin cannot be \"client\" at this point");
1094    };
1095
1096    // Step 2. If request’s timing allow failed flag is set, then return failure.
1097
1098    // Step 3. Let values be the result of getting, decoding, and splitting `Timing-Allow-Origin`
1099    // from response’s header list.
1100    let values: Vec<&str> = response
1101        .headers
1102        .get_all("Timing-Allow-Origin")
1103        .iter()
1104        .map(|header_value| header_value.to_str().unwrap_or(""))
1105        .collect();
1106
1107    // Step 4. If values contains "*", then return success.
1108    if values.contains(&"*") {
1109        return Ok(());
1110    }
1111
1112    // Step 5. If values contains the result of serializing a request origin with request, then
1113    // return success.
1114    if values
1115        .iter()
1116        .any(|header_str| *header_str == request_origin.ascii_serialization().as_ref())
1117    {
1118        return Ok(());
1119    }
1120
1121    // Step 6. If request’s mode is "navigate" and request’s current URL’s origin is not same origin
1122    // with request’s origin, then return failure.
1123    if request.mode == RequestMode::Navigate && request.current_url().origin() != *request_origin {
1124        return Err(());
1125    }
1126
1127    // Step 7. If request’s response tainting is "basic", then return success.
1128    if request.response_tainting == ResponseTainting::Basic {
1129        return Ok(());
1130    }
1131
1132    // Step 8. Return failure.
1133    Err(())
1134}
1135
1136// Convenience struct that implements Drop, for setting redirectEnd on function return
1137struct RedirectEndTimer(Option<ResourceFetchTimingContainer>);
1138
1139impl RedirectEndTimer {
1140    fn neuter(&mut self) {
1141        self.0 = None;
1142    }
1143}
1144
1145impl Drop for RedirectEndTimer {
1146    fn drop(&mut self) {
1147        let RedirectEndTimer(resource_fetch_timing_opt) = self;
1148
1149        resource_fetch_timing_opt.as_ref().map_or((), |t| {
1150            t.set_attribute(ResourceAttribute::RedirectEnd(RedirectEndValue::Zero));
1151        })
1152    }
1153}
1154
1155/// <https://fetch.spec.whatwg.org/#request-body-header-name>
1156static REQUEST_BODY_HEADER_NAMES: &[HeaderName] = &[
1157    CONTENT_ENCODING,
1158    CONTENT_LANGUAGE,
1159    CONTENT_LOCATION,
1160    CONTENT_TYPE,
1161];
1162
1163/// <https://fetch.spec.whatwg.org/#concept-response-location-url>
1164fn location_url_for_response(
1165    response: &Response,
1166    request_fragment: Option<&str>,
1167) -> Option<Result<ServoUrl, String>> {
1168    // Step 1. If response’s status is not a redirect status, then return null.
1169    assert!(
1170        response
1171            .actual_response()
1172            .status
1173            .try_code()
1174            .is_some_and(is_redirect_status)
1175    );
1176    // Step 2. Let location be the result of extracting header list values given `Location` and response’s header list.
1177    let mut location = response
1178        .actual_response()
1179        .headers
1180        .get(header::LOCATION)
1181        .and_then(|header_value| {
1182            HeaderValue::to_str(header_value)
1183                .map(|location_string| {
1184                    // Step 3. If location is a header value, then set location to the result of parsing location with response’s URL.
1185                    ServoUrl::parse_with_base(response.actual_response().url(), location_string)
1186                        .map_err(|error| error.to_string())
1187                })
1188                .ok()
1189        });
1190
1191    // Step 4. If location is a URL whose fragment is null, then set location’s fragment to requestFragment.
1192    if let Some(Ok(ref mut location)) = location &&
1193        location.fragment().is_none()
1194    {
1195        location.set_fragment(request_fragment);
1196    }
1197    // Step 5. Return location.
1198    location
1199}
1200
1201/// [HTTP redirect fetch](https://fetch.spec.whatwg.org/#http-redirect-fetch)
1202#[async_recursion]
1203pub async fn http_redirect_fetch(
1204    fetch_params: &mut FetchParams,
1205    mut response: Response,
1206    cors_flag: bool,
1207    target: Target<'async_recursion>,
1208    done_chan: &mut DoneChannel,
1209    context: &FetchContext,
1210) -> Response {
1211    let mut redirect_end_timer = RedirectEndTimer(Some(fetch_params.timing_info.clone()));
1212
1213    // Step 1. Let request be fetchParams’s request.
1214    let request = &mut fetch_params.request;
1215
1216    // Step 2. Let internalResponse be response, if response is not a filtered response; otherwise response’s internal response.
1217    assert!(response.return_internal);
1218
1219    // Step 3. Let locationURL be internalResponse’s location URL given request’s current URL’s fragment.
1220    let location_url = location_url_for_response(&response, request.current_url().fragment());
1221    response.actual_response_mut().location_url = location_url.clone();
1222
1223    let location_url = match location_url {
1224        // Step 4. If locationURL is null, then return response.
1225        None => return response,
1226        // Step 5. If locationURL is failure, then return a network error.
1227        Some(Err(err)) => {
1228            return Response::network_error(NetworkError::ResourceLoadError(
1229                "Location URL parse failure: ".to_owned() + &err,
1230            ));
1231        },
1232        // Step 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network error.
1233        Some(Ok(url)) if !matches!(url.scheme(), "http" | "https") => {
1234            return Response::network_error(NetworkError::UnsupportedScheme);
1235        },
1236        Some(Ok(url)) => url,
1237    };
1238
1239    // Step 1 of https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-fetchstart
1240    // TODO: check origin and timing allow check
1241    // start_time should equal redirect_start if nonzero; else fetch_start
1242    // updates start_time only if redirect_start is nonzero (implying TAO)
1243    fetch_params.timing_info.set_attributes(&[
1244        ResourceAttribute::RedirectStart(RedirectStartValue::FetchStart),
1245        ResourceAttribute::FetchStart,
1246        ResourceAttribute::StartTime(ResourceTimeValue::FetchStart),
1247        ResourceAttribute::StartTime(ResourceTimeValue::RedirectStart),
1248    ]);
1249
1250    // Step 7: If request’s redirect count is 20, then return a network error.
1251    if request.redirect_count >= 20 {
1252        return Response::network_error(NetworkError::TooManyRedirects);
1253    }
1254
1255    // Step 8: Increase request’s redirect count by 1.
1256    request.redirect_count += 1;
1257
1258    // Step 9. If request’s mode is "cors", locationURL includes credentials,
1259    // and request’s origin is not same origin with locationURL’s origin, then return a network error.
1260    let has_credentials = has_credentials(&location_url);
1261    if request.mode == RequestMode::CorsMode {
1262        let same_origin = match request.origin {
1263            Origin::Origin(ref origin) => *origin == location_url.origin(),
1264            Origin::Client => panic!(
1265                "Request origin should not be client for {}",
1266                request.current_url()
1267            ),
1268        };
1269        if !same_origin && has_credentials {
1270            return Response::network_error(NetworkError::CorsCredentials);
1271        }
1272    }
1273
1274    // Step 10. If request’s response tainting is "cors" and locationURL includes credentials, then return a network error.
1275    if cors_flag && has_credentials {
1276        return Response::network_error(NetworkError::CorsCredentials);
1277    }
1278
1279    // Step 11: If internalResponse’s status is not 303, request’s body is non-null, and request’s
1280    // body’s source is null, then return a network error.
1281    if response.actual_response().status != StatusCode::SEE_OTHER &&
1282        request.body.as_ref().is_some_and(|b| b.source_is_null())
1283    {
1284        return Response::network_error(NetworkError::ConnectionFailure);
1285    }
1286
1287    // Step 12. If one of the following is true
1288    if response
1289        .actual_response()
1290        .status
1291        .try_code()
1292        .is_some_and(|code| {
1293            // internalResponse’s status is 301 or 302 and request’s method is `POST`
1294            ((code == StatusCode::MOVED_PERMANENTLY || code == StatusCode::FOUND) &&
1295                request.method == Method::POST) ||
1296                // internalResponse’s status is 303 and request’s method is not `GET` or `HEAD`
1297                (code == StatusCode::SEE_OTHER &&
1298                    request.method != Method::HEAD &&
1299                    request.method != Method::GET)
1300        })
1301    {
1302        // Step 12.1. Set request’s method to `GET` and request’s body to null.
1303        request.method = Method::GET;
1304        request.body = None;
1305        // Step 12.2. For each headerName of request-body-header name, delete headerName from request’s header list.
1306        for name in REQUEST_BODY_HEADER_NAMES {
1307            request.headers.remove(name);
1308        }
1309    }
1310
1311    // Step 13: If request’s current URL’s origin is not same origin with locationURL’s origin, then
1312    // for each headerName of CORS non-wildcard request-header name, delete headerName from
1313    // request’s header list.
1314    if location_url.origin() != request.current_url().origin() {
1315        // This list currently only contains the AUTHORIZATION header
1316        // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
1317        request.headers.remove(AUTHORIZATION);
1318    }
1319
1320    // Step 14: If request’s body is non-null, then set request’s body to the body of the result of
1321    // safely extracting request’s body’s source.
1322    if let Some(body) = request.body.as_mut() {
1323        body.extract_source();
1324    }
1325
1326    // Steps 15-17 relate to timing, which is not implemented 1:1 with the spec.
1327
1328    // Step 18: Append locationURL to request’s URL list.
1329    request
1330        .url_list
1331        .push(UrlWithBlobClaim::from_url_without_having_claimed_blob(
1332            location_url,
1333        ));
1334
1335    // Step 19: Invoke set request’s referrer policy on redirect on request and internalResponse.
1336    set_requests_referrer_policy_on_redirect(request, response.actual_response());
1337
1338    // Step 20: Let recursive be true.
1339    // Step 21: If request’s redirect mode is "manual", then...
1340    let recursive_flag = request.redirect_mode != RedirectMode::Manual;
1341
1342    // Step 22: Return the result of running main fetch given fetchParams and recursive.
1343    let fetch_response = main_fetch(fetch_params, recursive_flag, target, done_chan, context).await;
1344
1345    // TODO: timing allow check
1346    fetch_params
1347        .timing_info
1348        .set_attribute(ResourceAttribute::RedirectEnd(
1349            RedirectEndValue::ResponseEnd,
1350        ));
1351    redirect_end_timer.neuter();
1352
1353    fetch_response
1354}
1355
1356/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
1357#[async_recursion]
1358#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
1359async fn http_network_or_cache_fetch(
1360    fetch_params: &mut FetchParams,
1361    authentication_fetch_flag: bool,
1362    cors_flag: bool,
1363    done_chan: &mut DoneChannel,
1364    context: &FetchContext,
1365) -> Response {
1366    // Step 2. Let httpFetchParams be null.
1367    let http_fetch_params: &mut FetchParams;
1368    let mut fetch_params_copy: FetchParams;
1369
1370    // Step 3. Let httpRequest be null. (See step 8 for initialization)
1371
1372    // Step 4. Let response be null.
1373    let mut response: Option<Response> = None;
1374
1375    // Step 7. Let the revalidatingFlag be unset.
1376    let mut revalidating_flag = false;
1377
1378    // TODO(#33616): Step 8. Run these steps, but abort when fetchParams is canceled:
1379    // Step 8.1. If request’s traversable for user prompts is "no-traversable"
1380    // and request’s redirect mode is "error", then set httpFetchParams to fetchParams and httpRequest to request.
1381    let http_request = if fetch_params.request.traversable_for_user_prompts ==
1382        TraversableForUserPrompts::NoTraversable &&
1383        fetch_params.request.redirect_mode == RedirectMode::Error
1384    {
1385        http_fetch_params = fetch_params;
1386        &mut http_fetch_params.request
1387    }
1388    // Step 8.2 Otherwise:
1389    else {
1390        // Step 8.2.1 - 8.2.3: Set httpRequest to a clone of request
1391        // and Set httpFetchParams to a copy of fetchParams.
1392        fetch_params_copy = std::mem::replace(
1393            fetch_params,
1394            FetchParams::new(
1395                fetch_params.request.clone(),
1396                fetch_params.timing_info.clone(),
1397            ),
1398        );
1399        http_fetch_params = &mut fetch_params_copy;
1400
1401        &mut http_fetch_params.request
1402    };
1403
1404    // Step 8.3: Let includeCredentials be true if one of:
1405    let include_credentials = match http_request.credentials_mode {
1406        // request’s credentials mode is "include"
1407        CredentialsMode::Include => true,
1408        // request’s credentials mode is "same-origin" and request’s response tainting is "basic"
1409        CredentialsMode::CredentialsSameOrigin
1410            if http_request.response_tainting == ResponseTainting::Basic =>
1411        {
1412            true
1413        },
1414        _ => false,
1415    };
1416
1417    // Step 8.4: If Cross-Origin-Embedder-Policy allows credentials with request returns false, then
1418    // set includeCredentials to false.
1419    // TODO(#33616): Requires request's client object
1420
1421    // Step 8.5 Let contentLength be httpRequest’s body’s length, if httpRequest’s body is non-null;
1422    // otherwise null.
1423    let content_length = http_request
1424        .body
1425        .as_ref()
1426        .and_then(|body| body.len().map(|size| size as u64));
1427
1428    // Step 8.6 Let contentLengthHeaderValue be null.
1429    let mut content_length_header_value = None;
1430
1431    // Step 8.7 If httpRequest’s body is null and httpRequest’s method is `POST` or `PUT`,
1432    // then set contentLengthHeaderValue to `0`.
1433    if http_request.body.is_none() && matches!(http_request.method, Method::POST | Method::PUT) {
1434        content_length_header_value = Some(0);
1435    }
1436
1437    // Step 8.8 If contentLength is non-null, then set contentLengthHeaderValue to contentLength,
1438    // serialized and isomorphic encoded.
1439    // NOTE: The header will later be serialized using HeaderMap::typed_insert
1440    if let Some(content_length) = content_length {
1441        content_length_header_value = Some(content_length);
1442    };
1443
1444    // Step 8.9 If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue)
1445    // to httpRequest’s header list.
1446    if let Some(content_length_header_value) = content_length_header_value {
1447        http_request
1448            .headers
1449            .typed_insert(ContentLength(content_length_header_value));
1450    }
1451
1452    // Step 8.10 If contentLength is non-null and httpRequest’s keepalive is true, then:
1453    if http_request.keep_alive &&
1454        let Some(content_length) = content_length
1455    {
1456        // Step 8.10.1. Let inflightKeepaliveBytes be 0.
1457        // Step 8.10.2. Let group be httpRequest’s client’s fetch group.
1458        // Step 8.10.3. Let inflightRecords be the set of fetch records
1459        // in group whose request’s keepalive is true and done flag is unset.
1460        let in_flight_keep_alive_bytes: u64 = context
1461            .in_flight_keep_alive_records
1462            .lock()
1463            .get(
1464                &http_request
1465                    .pipeline_id
1466                    .expect("Must always set a pipeline ID for keep-alive requests"),
1467            )
1468            .map(|records| {
1469                // Step 8.10.4. For each fetchRecord of inflightRecords:
1470                // Step 8.10.4.1. Let inflightRequest be fetchRecord’s request.
1471                // Step 8.10.4.2. Increment inflightKeepaliveBytes by inflightRequest’s body’s length.
1472                records
1473                    .iter()
1474                    .map(|record| {
1475                        if record.request_id == http_request.id {
1476                            // Don't double count for this request. We have already added it in
1477                            // `fetch::methods::fetch`
1478                            0
1479                        } else {
1480                            record.keep_alive_body_length
1481                        }
1482                    })
1483                    .sum()
1484            })
1485            .unwrap_or_default();
1486        // Step 8.10.5. If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
1487        if content_length + in_flight_keep_alive_bytes > 64 * 1024 {
1488            return Response::network_error(NetworkError::TooManyInFlightKeepAliveRequests);
1489        }
1490    }
1491
1492    // Step 8.11: If httpRequest’s referrer is a URL, then:
1493    match http_request.referrer {
1494        Referrer::ReferrerUrl(ref http_request_referrer) |
1495        Referrer::Client(ref http_request_referrer) => {
1496            // Step 8.11.1: Let referrerValue be httpRequest’s referrer, serialized and isomorphic
1497            // encoded.
1498            if let Ok(referer) = http_request_referrer.as_str().parse::<Referer>() {
1499                // Step 8.11.2: Append (`Referer`, referrerValue) to httpRequest’s header list.
1500                http_request.headers.typed_insert(referer);
1501            } else {
1502                // This error should only happen in cases where hyper and rust-url disagree
1503                // about how to parse a referer.
1504                // https://github.com/servo/servo/issues/24175
1505                error!("Failed to parse {} as referrer", http_request_referrer);
1506            }
1507        },
1508        _ => {},
1509    };
1510
1511    // Step 8.12 Append a request `Origin` header for httpRequest.
1512    append_a_request_origin_header(http_request);
1513
1514    // Step 8.13 Append the Fetch metadata headers for httpRequest.
1515    append_the_fetch_metadata_headers(http_request);
1516
1517    // Step 8.14: If httpRequest’s initiator is "prefetch", then set a structured field value given
1518    // (`Sec-Purpose`, the token "prefetch") in httpRequest’s header list.
1519    if http_request.initiator == Initiator::Prefetch &&
1520        let Ok(value) = HeaderValue::from_str("prefetch")
1521    {
1522        http_request.headers.insert("Sec-Purpose", value);
1523    }
1524
1525    // Step 8.15: If httpRequest’s header list does not contain `User-Agent`, then user agents
1526    // should append (`User-Agent`, default `User-Agent` value) to httpRequest’s header list.
1527    if !http_request.headers.contains_key(header::USER_AGENT) {
1528        http_request
1529            .headers
1530            .typed_insert::<UserAgent>(context.user_agent.parse().unwrap());
1531    }
1532
1533    // Steps 8.16 to 8.18
1534    append_cache_data_to_headers(http_request);
1535
1536    // Step 8.19: If httpRequest’s header list contains `Range`, then append (`Accept-Encoding`,
1537    // `identity`) to httpRequest’s header list.
1538    if http_request.headers.contains_key(header::RANGE) &&
1539        let Ok(value) = HeaderValue::from_str("identity")
1540    {
1541        http_request.headers.insert("Accept-Encoding", value);
1542    }
1543
1544    // Step 8.20: Modify httpRequest’s header list per HTTP. Do not append a given header if
1545    // httpRequest’s header list contains that header’s name.
1546    // `Accept`, `Accept-Charset`, and `Accept-Language` must not be included at this point.
1547    http_request.headers.remove(header::HOST);
1548    // unlike http_loader, we should not set the accept header here
1549    set_default_accept_encoding(&mut http_request.headers);
1550
1551    let current_url = http_request.current_url();
1552
1553    // Step 8.21: If includeCredentials is true, then:
1554    // TODO some of this step can't be implemented yet
1555    if include_credentials {
1556        // Substep 1
1557        // TODO http://mxr.mozilla.org/servo/source/components/net/http_loader.rs#504
1558        // XXXManishearth http_loader has block_cookies: support content blocking here too
1559        set_request_cookies(
1560            &current_url,
1561            &mut http_request.headers,
1562            &context.state.cookie_jar,
1563        );
1564        // Substep 2
1565        if !http_request.headers.contains_key(header::AUTHORIZATION) {
1566            // Substep 3
1567            let mut authorization_value = None;
1568
1569            // Substep 4
1570            if let Some(basic) = auth_from_cache(&context.state.auth_cache, &current_url.origin()) &&
1571                (!http_request.use_url_credentials || !has_credentials(&current_url))
1572            {
1573                authorization_value = Some(basic);
1574            }
1575
1576            // Substep 5
1577            if authentication_fetch_flag &&
1578                authorization_value.is_none() &&
1579                has_credentials(&current_url)
1580            {
1581                authorization_value = Some(Authorization::basic(
1582                    current_url.username(),
1583                    current_url.password().unwrap_or(""),
1584                ));
1585            }
1586
1587            // Substep 6
1588            if let Some(basic) = authorization_value {
1589                http_request.headers.typed_insert(basic);
1590            }
1591        }
1592    }
1593
1594    // TODO(#33616) Step 8.22 If there’s a proxy-authentication entry, use it as appropriate.
1595    let should_wait = {
1596        // Enter critical section on cache entry.
1597        let mut cache_guard = block_for_cache_ready(
1598            context,
1599            http_request,
1600            done_chan,
1601            &mut revalidating_flag,
1602            &mut response,
1603        )
1604        .await;
1605
1606        // TODO(#33616): Step 9. If aborted, then return the appropriate network error for fetchParams.
1607
1608        // Step 10. If response is null, then:
1609        if response.is_none() {
1610            // Step 10.1 If httpRequest’s cache mode is "only-if-cached", then return a network error.
1611            if http_request.cache_mode == CacheMode::OnlyIfCached {
1612                // Exit critical section of cache entry.
1613                return Response::network_error(NetworkError::CacheError);
1614            }
1615
1616            // Step 10.2 Let forwardResponse be the result of running HTTP-network fetch given httpFetchParams,
1617            // includeCredentials, and isNewConnectionFetch.
1618            drop(cache_guard);
1619            let forward_response =
1620                http_network_fetch(http_fetch_params, include_credentials, done_chan, context)
1621                    .await;
1622
1623            let http_request = &mut http_fetch_params.request;
1624            let request_key = CacheKey::new(http_request);
1625            cache_guard = context
1626                .state
1627                .http_cache
1628                .get_or_guard(request_key.clone())
1629                .await;
1630            // Step 10.3 If httpRequest’s method is unsafe and forwardResponse’s status is in the range 200 to 399,
1631            // inclusive, invalidate appropriate stored responses in httpCache, as per the
1632            // "Invalidating Stored Responses" chapter of HTTP Caching, and set storedResponse to null.
1633            if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() {
1634                if let Some(guard) = cache_guard.try_as_mut() {
1635                    invalidate_cached_resources(guard);
1636                }
1637                context
1638                    .state
1639                    .http_cache
1640                    .invalidate_related_urls(http_request, &forward_response, &request_key)
1641                    .await;
1642            }
1643
1644            // Step 10.4 If the revalidatingFlag is set and forwardResponse’s status is 304, then:
1645            if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED {
1646                // Ensure done_chan is None,
1647                // since the network response will be replaced by the revalidated stored one.
1648                *done_chan = None;
1649                if let Some(guard) = cache_guard.try_as_mut() {
1650                    response = refresh(http_request, forward_response.clone(), done_chan, guard);
1651                }
1652
1653                match &mut response {
1654                    Some(response) => {
1655                        response.cache_state = CacheState::Validated;
1656                    },
1657                    // The validated entry was no longer in the cache when we expected it to be.
1658                    None => {
1659                        *done_chan = None;
1660                        return Response::network_error(NetworkError::CacheError);
1661                    },
1662                }
1663            }
1664
1665            // Step 10.5 If response is null, then:
1666            if response.is_none() {
1667                // Step 10.5.1 Set response to forwardResponse.
1668                let forward_response = response.insert(forward_response);
1669
1670                // Per https://httpwg.org/specs/rfc9111.html#response.cacheability we must not cache responses
1671                // if the No-Store directive is present
1672                if http_request.cache_mode != CacheMode::NoStore {
1673                    // Step 10.5.2 Store httpRequest and forwardResponse in httpCache, as per the
1674                    //             "Storing Responses in Caches" chapter of HTTP Caching.
1675                    cache_guard.insert(http_request, forward_response);
1676                }
1677            }
1678            false
1679        } else {
1680            true
1681        }
1682    }; // Exit Critical Section on cache entry
1683
1684    if should_wait {
1685        // If the cache constructed a response, and that is still receiving from the network,
1686        // we must wait for it to finish in case it is still receiving from the network.
1687        // Note: this means only the fetch from which the original network response originated
1688        // will be able to stream it; all others receive a cached response in one chunk.
1689        wait_for_inflight_requests(done_chan, &mut response).await;
1690    }
1691
1692    let http_request = &mut http_fetch_params.request;
1693    let mut response = response.unwrap();
1694
1695    // Step 11. Set response’s URL list to a clone of httpRequest’s URL list.
1696    response.url_list = http_request
1697        .url_list
1698        .iter()
1699        .map(|claimed_url| claimed_url.url())
1700        .collect();
1701
1702    // Step 12. If httpRequest’s header list contains `Range`, then set response’s range-requested flag.
1703    if http_request.headers.contains_key(RANGE) {
1704        response.range_requested = true;
1705    }
1706
1707    // Step 13. Set response’s request-includes-credentials to includeCredentials.
1708    response.request_includes_credentials = include_credentials;
1709
1710    // Step 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
1711    // includeCredentials is true, and request’s window is an environment settings object, then:
1712    // TODO(#33616): Figure out what to do with request window objects
1713    // NOTE: Requiring a WWW-Authenticate header here is ad-hoc, but seems to match what other browsers are
1714    // doing. See Step 14.1.
1715    if response.status.try_code() == Some(StatusCode::UNAUTHORIZED) &&
1716        !cors_flag &&
1717        include_credentials &&
1718        response.headers.contains_key(WWW_AUTHENTICATE)
1719    {
1720        // TODO: Step 14.1 Spec says requires testing on multiple WWW-Authenticate headers
1721
1722        let request = &mut fetch_params.request;
1723
1724        // Step 14.2 If request’s body is non-null, then:
1725        // “If request’s body’s source is null, then return a network error.”
1726        if request
1727            .body
1728            .as_ref()
1729            .is_some_and(|body| body.source_is_null())
1730        {
1731            return Response::network_error(NetworkError::ConnectionFailure);
1732        }
1733
1734        // Step 14.3 If request’s use-URL-credentials flag is unset or isAuthenticationFetch is true, then:
1735        if !request.use_url_credentials || authentication_fetch_flag {
1736            let Some(credentials) = context
1737                .state
1738                .request_authentication(request, &response)
1739                .await
1740            else {
1741                return response;
1742            };
1743
1744            if let Err(err) = request
1745                .current_url_mut()
1746                .set_username(&credentials.username)
1747            {
1748                error!("error setting username for url: {:?}", err);
1749                return response;
1750            };
1751
1752            if let Err(err) = request
1753                .current_url_mut()
1754                .set_password(Some(&credentials.password))
1755            {
1756                error!("error setting password for url: {:?}", err);
1757                return response;
1758            };
1759        }
1760
1761        // Make sure this is set to None,
1762        // since we're about to start a new `http_network_or_cache_fetch`.
1763        *done_chan = None;
1764
1765        // Step 14.4 Set response to the result of running HTTP-network-or-cache fetch given fetchParams and true.
1766        response = http_network_or_cache_fetch(
1767            fetch_params,
1768            true, /* authentication flag */
1769            cors_flag,
1770            done_chan,
1771            context,
1772        )
1773        .await;
1774    }
1775
1776    // Step 15. If response’s status is 407, then:
1777    if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
1778        let request = &mut fetch_params.request;
1779        // Step 15.1 If request’s traversable for user prompts is "no-traversable", then return a network error.
1780
1781        if request.traversable_for_user_prompts == TraversableForUserPrompts::NoTraversable {
1782            return Response::network_error(NetworkError::ResourceLoadError(
1783                "Can't find Window object".into(),
1784            ));
1785        }
1786
1787        // (Step 15.2 does not exist, requires testing on Proxy-Authenticate headers)
1788
1789        // TODO(#33616): Step 15.3 If fetchParams is canceled, then return
1790        // the appropriate network error for fetchParams.
1791
1792        // Step 15.4 Prompt the end user as appropriate in request’s window
1793        // window and store the result as a proxy-authentication entry.
1794        let Some(credentials) = context
1795            .state
1796            .request_authentication(request, &response)
1797            .await
1798        else {
1799            return response;
1800        };
1801
1802        // Store the credentials as a proxy-authentication entry.
1803        let entry = AuthCacheEntry {
1804            user_name: credentials.username,
1805            password: credentials.password,
1806        };
1807        {
1808            let mut auth_cache = context.state.auth_cache.write();
1809            let key = request
1810                .current_url()
1811                .origin()
1812                .ascii_serialization()
1813                .into_owned();
1814            auth_cache.entries.insert(key, entry);
1815        }
1816
1817        // Make sure this is set to None,
1818        // since we're about to start a new `http_network_or_cache_fetch`.
1819        *done_chan = None;
1820
1821        // Step 15.5 Set response to the result of running HTTP-network-or-cache fetch given fetchParams.
1822        response = http_network_or_cache_fetch(
1823            fetch_params,
1824            false, /* authentication flag */
1825            cors_flag,
1826            done_chan,
1827            context,
1828        )
1829        .await;
1830    }
1831
1832    // TODO(#33616): Step 16. If all of the following are true:
1833    // * response’s status is 421
1834    // * isNewConnectionFetch is false
1835    // * request’s body is null, or request’s body is non-null and request’s body’s source is non-null
1836    // then: [..]
1837
1838    // Step 17. If isAuthenticationFetch is true, then create an authentication entry for request and the given realm.
1839    if authentication_fetch_flag {
1840        // TODO(#33616)
1841    }
1842
1843    // Step 18. Return response.
1844    response
1845}
1846
1847/// If the cache is not ready to construct a response, wait.
1848///
1849/// The cache is not ready if a previous fetch checked the cache, found nothing,
1850/// and moved on to a network fetch, and hasn't updated the cache yet with a pending resource.
1851///
1852/// Note that this is a different workflow from the one involving `wait_for_cached_response`.
1853/// That one happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1854#[servo_tracing::instrument(skip_all)]
1855async fn block_for_cache_ready<'a>(
1856    context: &'a FetchContext,
1857    http_request: &mut Request,
1858    done_chan: &mut DoneChannel,
1859    revalidating_flag: &mut bool,
1860    response: &mut Option<Response>,
1861) -> CachedResourcesOrGuard<'a> {
1862    let entry_key = CacheKey::new(http_request);
1863    let guard_result = context.state.http_cache.get_or_guard(entry_key).await;
1864
1865    match guard_result {
1866        CachedResourcesOrGuard::Guard(_) => {
1867            *done_chan = None;
1868        },
1869        CachedResourcesOrGuard::Value(ref cached_resources) => {
1870            // TODO(#33616): Step 8.23 Set httpCache to the result of determining the
1871            // HTTP cache partition, given httpRequest.
1872            // Step 8.25.1 Set storedResponse to the result of selecting a response from the httpCache,
1873            //              possibly needing validation, as per the "Constructing Responses from Caches"
1874            //              chapter of HTTP Caching, if any.
1875            let stored_response = construct_response(http_request, done_chan, cached_resources);
1876            // Step 8.25.2 If storedResponse is non-null, then:
1877            if let Some(response_from_cache) = stored_response {
1878                let validation_status = &response_from_cache.validation_status;
1879                let revalidation_guard = &response_from_cache.revalidation_guard;
1880
1881                // Substep 1, 2, 3, 4
1882                let (cached_response, needs_synchronous_revalidation) =
1883                    match (http_request.cache_mode, &http_request.mode) {
1884                        (CacheMode::ForceCache, _) => (Some(response_from_cache.response), false),
1885                        (CacheMode::OnlyIfCached, &RequestMode::SameOrigin) => {
1886                            (Some(response_from_cache.response), false)
1887                        },
1888                        (CacheMode::OnlyIfCached, _) |
1889                        (CacheMode::NoStore, _) |
1890                        (CacheMode::Reload, _) => (None, false),
1891                        (_, _) => (
1892                            Some(response_from_cache.response),
1893                            *validation_status ==
1894                                (ValidationStatus::Stale {
1895                                    revalidate_in_background: false,
1896                                }),
1897                        ),
1898                    };
1899
1900                if needs_synchronous_revalidation {
1901                    *revalidating_flag = true;
1902                    let response_headers = &cached_response
1903                        .as_ref()
1904                        .expect("needs_synchronous_revalidation implies a cached response")
1905                        .headers;
1906                    // Substep 5
1907                    if let Some(http_date) = response_headers.typed_get::<LastModified>() {
1908                        let http_date: SystemTime = http_date.into();
1909                        http_request
1910                            .headers
1911                            .typed_insert(IfModifiedSince::from(http_date));
1912                    }
1913                    if let Some(entity_tag) = response_headers.get(header::ETAG) {
1914                        http_request
1915                            .headers
1916                            .insert(header::IF_NONE_MATCH, entity_tag.clone());
1917                    }
1918                } else {
1919                    // Substep 6
1920                    // If it's a stale-while-revalidate response, also refresh it in the background.
1921                    let revalidate_in_background = *validation_status ==
1922                        (ValidationStatus::Stale {
1923                            revalidate_in_background: true,
1924                        });
1925                    if revalidate_in_background && cached_response.is_some() {
1926                        spawn_stale_while_revalidate(
1927                            context,
1928                            http_request,
1929                            revalidation_guard.clone(),
1930                        );
1931                    }
1932                    *response = cached_response;
1933                    if let Some(response) = response {
1934                        response.cache_state = CacheState::Local;
1935                    }
1936                }
1937                if response.is_none() {
1938                    // Ensure the done chan is not set if we're not using the cached response,
1939                    // as the cache might have set it to Some if it constructed a pending response.
1940                    *done_chan = None;
1941                }
1942            }
1943        },
1944    }
1945    guard_result
1946}
1947
1948/// The cached (stale) response has already been returned to the caller; here we
1949/// fire off an independent fetch whose only purpose is to refresh the stored response.
1950fn spawn_stale_while_revalidate(
1951    context: &FetchContext,
1952    http_request: &Request,
1953    revalidation_guard: StdArc<AtomicBool>,
1954) {
1955    // Only proceed if we are the one who flips the guard from `false` to `true`.
1956    if revalidation_guard
1957        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1958        .is_err()
1959    {
1960        return;
1961    }
1962
1963    // By setting `CacheMode::NoCache` to the cloned request,
1964    // we ensure that the background revalidation fetch will always go to the network, and preventing an inifinite loop.
1965    let mut revalidation_request = http_request.clone();
1966    revalidation_request.cache_mode = CacheMode::NoCache;
1967
1968    // A background revalidation must not itself spin up service workers
1969    revalidation_request.service_workers_mode = ServiceWorkersMode::None;
1970
1971    let context = context.clone();
1972    debug!(
1973        "spawning stale-while-revalidate background revalidation for {:?}",
1974        revalidation_request.current_url()
1975    );
1976    spawn_task(async move {
1977        let mut target = DiscardFetch;
1978
1979        let _ = fetch(revalidation_request, &mut target, &context).await;
1980        revalidation_guard.store(false, Ordering::Release);
1981    });
1982}
1983
1984/// Wait for a cached response from channel.
1985/// Happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1986async fn wait_for_inflight_requests(done_chan: &mut DoneChannel, response: &mut Option<Response>) {
1987    if let Some(ref mut ch) = *done_chan {
1988        // The cache constructed a response with a body of ResponseBody::Receiving.
1989        // We wait for the response in the cache to "finish",
1990        // with a body of either Done or Cancelled.
1991        assert!(response.is_some());
1992
1993        loop {
1994            match ch.1.recv().await {
1995                Some(Data::ContentLength(_)) | Some(Data::Payload(_)) | Some(Data::Error(_)) => {},
1996                Some(Data::Done) => break, // Return the full response as if it was initially cached as such.
1997                Some(Data::Cancelled) => {
1998                    // The response was cancelled while the fetch was ongoing.
1999                    break;
2000                },
2001                None => panic!("HTTP cache should always send Done or Cancelled"),
2002            }
2003        }
2004    }
2005    // Set done_chan back to None, it's cache-related usefulness ends here.
2006    *done_chan = None;
2007}
2008
2009/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
2010///
2011/// This is obtained from [cross_origin_resource_policy_check]
2012#[derive(PartialEq)]
2013enum CrossOriginResourcePolicy {
2014    Allowed,
2015    Blocked,
2016}
2017
2018enum ForNavigation {
2019    #[expect(dead_code)]
2020    Yes,
2021    No,
2022}
2023
2024/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
2025fn cross_origin_resource_policy_check(
2026    origin: &Origin,
2027    request_client: &RequestClient,
2028    response: &Response,
2029    for_navigation: ForNavigation,
2030) -> CrossOriginResourcePolicy {
2031    // Step 1. Set forNavigation to false if it is not given.
2032    //
2033    // That's the default value of the enum
2034
2035    // Step 2. Let embedderPolicy be settingsObject’s policy container’s embedder policy.
2036    let embedder_policy = &request_client.policy_container.embedder_policy;
2037
2038    // Step 3. If the cross-origin resource policy internal check with origin, "unsafe-none",
2039    // response, and forNavigation returns blocked, then return blocked.
2040    if cross_origin_resource_policy_internal_check(
2041        origin,
2042        EmbedderPolicyValue::UnsafeNone,
2043        response,
2044        &for_navigation,
2045    ) == CrossOriginResourcePolicy::Blocked
2046    {
2047        return CrossOriginResourcePolicy::Blocked;
2048    }
2049
2050    // TODO Step 4. If the cross-origin resource policy internal check with origin,
2051    // embedderPolicy’s report only value, response, and forNavigation returns blocked, then queue
2052    // a cross-origin embedder policy CORP violation report with response, settingsObject,
2053    // destination, and true.
2054
2055    // Step 5. If the cross-origin resource policy internal check with origin, embedderPolicy’s
2056    // value, response, and forNavigation returns allowed, then return allowed.
2057    if cross_origin_resource_policy_internal_check(
2058        origin,
2059        embedder_policy.value,
2060        response,
2061        &for_navigation,
2062    ) == CrossOriginResourcePolicy::Allowed
2063    {
2064        return CrossOriginResourcePolicy::Allowed;
2065    }
2066
2067    // TODO Step 6. Queue a cross-origin embedder policy CORP violation report with response,
2068    // settingsObject, destination, and false.
2069
2070    // Step 7. Return blocked.
2071    CrossOriginResourcePolicy::Blocked
2072}
2073
2074/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check>
2075fn cross_origin_resource_policy_internal_check(
2076    origin: &Origin,
2077    embedder_policy_value: EmbedderPolicyValue,
2078    response: &Response,
2079    for_navigation: &ForNavigation,
2080) -> CrossOriginResourcePolicy {
2081    // Step 1. If forNavigation is true and embedderPolicyValue is "unsafe-none", then return allowed.
2082    if let ForNavigation::Yes = for_navigation &&
2083        let EmbedderPolicyValue::UnsafeNone = embedder_policy_value
2084    {
2085        return CrossOriginResourcePolicy::Allowed;
2086    }
2087
2088    // Step 2. Let policy be the result of getting `Cross-Origin-Resource-Policy` from response’s header list.
2089    let policy = response
2090        .headers
2091        .get(HeaderName::from_static("cross-origin-resource-policy"))
2092        .and_then(|h| h.to_str().ok());
2093
2094    // Step 3. If policy is neither `same-origin`, `same-site`, nor `cross-origin`, then set policy to null.
2095    let policy = policy
2096        .filter(|&s| s == "same-origin" || s == "same-site" || s == "cross-origin")
2097        // Step 4. If policy is null, then switch on embedderPolicyValue:
2098        .or(match embedder_policy_value {
2099            // Do nothing.
2100            EmbedderPolicyValue::UnsafeNone => None,
2101            // Set policy to `same-origin`.
2102            EmbedderPolicyValue::RequireCorp => Some("same-origin"),
2103        });
2104
2105    // Step 5. Switch on policy:
2106    match policy {
2107        Some("same-origin") => {
2108            // If origin is same origin with response’s URL’s origin, then return allowed.
2109            if let Origin::Origin(request_origin) = origin &&
2110                response
2111                    .url()
2112                    .is_some_and(|url| request_origin == &url.origin())
2113            {
2114                return CrossOriginResourcePolicy::Allowed;
2115            }
2116
2117            // Otherwise, return blocked.
2118            CrossOriginResourcePolicy::Blocked
2119        },
2120        Some("same-site") => {
2121            if let Some(response_url) = response.url() {
2122                // If all of the following are true
2123                // origin is schemelessly same site with response’s URL’s origin
2124                // origin’s scheme is "https" or response’s URL’s scheme is not "https"
2125                if let Origin::Origin(request_origin) = origin &&
2126                    is_schemelessy_same_site(request_origin, &response_url.origin()) &&
2127                    (request_origin.scheme() == Some("https") ||
2128                        response_url.scheme() != "https")
2129                {
2130                    return CrossOriginResourcePolicy::Allowed;
2131                }
2132            }
2133            // Otherwise, return blocked.
2134            CrossOriginResourcePolicy::Blocked
2135        },
2136        // null / 'cross-origin'
2137        // Return allowed.
2138        _ => CrossOriginResourcePolicy::Allowed,
2139    }
2140}
2141
2142// Convenience struct that implements Done, for setting responseEnd on function return
2143struct ResponseEndTimer(Option<ResourceFetchTimingContainer>);
2144
2145impl ResponseEndTimer {
2146    fn neuter(&mut self) {
2147        self.0 = None;
2148    }
2149}
2150
2151impl Drop for ResponseEndTimer {
2152    fn drop(&mut self) {
2153        let ResponseEndTimer(resource_fetch_timing_opt) = self;
2154
2155        resource_fetch_timing_opt.as_ref().map_or((), |t| {
2156            t.set_attribute(ResourceAttribute::ResponseEnd);
2157        })
2158    }
2159}
2160
2161/// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch)
2162#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
2163async fn http_network_fetch(
2164    fetch_params: &mut FetchParams,
2165    credentials_flag: bool,
2166    done_chan: &mut DoneChannel,
2167    context: &FetchContext,
2168) -> Response {
2169    let mut response_end_timer = ResponseEndTimer(Some(fetch_params.timing_info.clone()));
2170
2171    // Step 1: Let request be fetchParams’s request.
2172    let request = &mut fetch_params.request;
2173
2174    // TODO Step 2. If request’s client is offline, then return a network error.
2175
2176    // TODO Step 3. Let response be null.
2177
2178    // TODO Step 4. Let timingInfo be fetchParams’s timing info.
2179
2180    // TODO Step 5. Let networkPartitionKey be the result of determining the network partition key
2181    // given request.
2182
2183    let url = request.current_url();
2184    let request_id = request.id.0;
2185    if log_enabled!(log::Level::Info) {
2186        info!("{:?} request for {}", request.method, url);
2187        for header in request.headers.iter() {
2188            debug!(" - {:?}", header);
2189        }
2190    }
2191
2192    // The receiver will receive true if there has been an error streaming the request body.
2193    let (fetch_terminated_sender, mut fetch_terminated_receiver) = unbounded_channel();
2194
2195    let body = request.body.as_ref().map(|body| body.clone_stream());
2196
2197    if body.is_none() {
2198        // There cannot be an error streaming a non-existent body.
2199        // However in such a case the channel will remain unused
2200        // and drop inside `obtain_response`.
2201        // Send the confirmation now, ensuring the receiver will not dis-connect first.
2202        let _ = fetch_terminated_sender.send(false);
2203    }
2204
2205    // Step 6. Let newConnection be "yes" if forceNewConnection is true; otherwise "no".
2206
2207    // Step 7. Switch on request’s mode:
2208    let (response_stream, msg) = match &request.mode {
2209        // Let connection be the result of obtaining a WebSocket connection, given request’s current URL.
2210        RequestMode::WebSocket {
2211            protocols,
2212            original_url: _,
2213        } => {
2214            // https://fetch.spec.whatwg.org/#websocket-opening-handshake
2215
2216            let (resource_event_sender, dom_action_receiver) = {
2217                let mut websocket_chan = context.websocket_chan.as_ref().unwrap().lock();
2218                (
2219                    websocket_chan.sender.clone(),
2220                    websocket_chan.receiver.take().unwrap(),
2221                )
2222            };
2223
2224            let mut tls_config = create_tls_config(
2225                context.ca_certificates.clone(),
2226                context.ignore_certificate_errors,
2227                context.state.override_manager.clone(),
2228            );
2229            tls_config.alpn_protocols = vec!["http/1.1".into()];
2230
2231            let response = match start_websocket(
2232                context.state.clone(),
2233                resource_event_sender,
2234                protocols,
2235                request,
2236                tls_config,
2237                dom_action_receiver,
2238            )
2239            .await
2240            {
2241                Ok(response) => response,
2242                Err(error) => {
2243                    return Response::network_error(NetworkError::WebsocketConnectionFailure(
2244                        format!("{error:?}"),
2245                    ));
2246                },
2247            };
2248
2249            let response = response.map(|r| match r {
2250                Some(body) => Full::from(body).map_err(|_| unreachable!()).boxed(),
2251                None => http_body_util::Empty::new()
2252                    .map_err(|_| unreachable!())
2253                    .boxed(),
2254            });
2255            (Decoder::detect(response, url.is_secure_scheme()), None)
2256        },
2257        // Let connection be the result of obtaining a connection, given networkPartitionKey,
2258        // request’s current URL, includeCredentials, and newConnection.
2259        _ => {
2260            let response_future = obtain_response(
2261                fetch_params,
2262                &context.state.client,
2263                &url,
2264                body,
2265                Some(&request_id),
2266                context,
2267                fetch_terminated_sender,
2268            );
2269
2270            // This will only get the headers, the body is read later
2271            let (response_stream, msg) = match response_future.await {
2272                Ok(wrapped_response) => wrapped_response,
2273                Err(error) => return Response::network_error(error),
2274            };
2275            (response_stream, msg)
2276        },
2277    };
2278
2279    let request = &mut fetch_params.request;
2280
2281    if log_enabled!(log::Level::Info) {
2282        debug!("{:?} response for {}", response_stream.version(), url);
2283        for header in response_stream.headers().iter() {
2284            debug!(" - {:?}", header);
2285        }
2286    }
2287
2288    // Check if there was an error while streaming the request body.
2289    //
2290    match fetch_terminated_receiver.recv().await {
2291        Some(true) => return Response::network_error(NetworkError::ConnectionFailure),
2292        Some(false) => {},
2293        _ => warn!("Failed to receive confirmation request was streamed without error."),
2294    }
2295
2296    let timing = fetch_params.timing_info.inner().clone();
2297    let mut response = Response::new(url.clone(), timing);
2298
2299    if let Some(handshake_info) = response_stream.extensions().get::<TlsHandshakeInfo>() {
2300        let mut hsts_enabled = url
2301            .host_str()
2302            .is_some_and(|host| context.state.hsts_list.read().is_host_secure(host));
2303
2304        if url.scheme() == "https" &&
2305            let Some(strict_transport_security) = response_stream
2306                .headers()
2307                .typed_get::<StrictTransportSecurity>()
2308        {
2309            // max-age > 0 enables HSTS, max-age = 0 disables it (RFC 6797 Section 6.1.1)
2310            hsts_enabled = strict_transport_security.max_age().as_secs() > 0;
2311        }
2312        response.tls_security_info = Some(build_tls_security_info(handshake_info, hsts_enabled));
2313    }
2314
2315    let status_text = response_stream
2316        .extensions()
2317        .get::<ReasonPhrase>()
2318        .map(ReasonPhrase::as_bytes)
2319        .or_else(|| {
2320            response_stream
2321                .status()
2322                .canonical_reason()
2323                .map(str::as_bytes)
2324        })
2325        .map(Vec::from)
2326        .unwrap_or_default();
2327    response.status = HttpStatus::new(response_stream.status(), status_text);
2328
2329    info!(
2330        "got {:?} response for {:?}",
2331        response_stream.status(),
2332        request.url()
2333    );
2334    response.headers = response_stream.headers().clone();
2335    response.referrer = request.referrer.to_url().cloned();
2336    response.referrer_policy = request.referrer_policy;
2337
2338    let response_body = response.body.clone();
2339
2340    // We're about to spawn a future to be waited on here
2341    let (done_sender, done_receiver) = unbounded_channel();
2342    *done_chan = Some((done_sender.clone(), done_receiver));
2343
2344    let cancellation_listener = context.cancellation_listener.clone();
2345    if cancellation_listener.cancelled() {
2346        return Response::network_error(NetworkError::LoadCancelled);
2347    }
2348
2349    #[cfg(feature = "devtools")]
2350    let (devtools_request, status, headers, devtools_chan) = {
2351        if let Some(ref sender) = context.devtools_chan &&
2352            let Some(msg) = msg
2353        {
2354            send_request_to_devtools(msg, sender);
2355        }
2356        (
2357            request.clone(),
2358            response.status.clone(),
2359            response.headers.clone(),
2360            context.devtools_chan.clone(),
2361        )
2362    };
2363
2364    let done_sender2 = done_sender.clone();
2365    let done_sender3 = done_sender.clone();
2366    let timing_ptr2 = fetch_params.timing_info.clone();
2367    let timing_ptr3 = fetch_params.timing_info.clone();
2368    let url1 = request.url();
2369    let url2 = url1.clone();
2370
2371    let prealloc_size: usize = if let Some(possible_length) = response_stream
2372        .headers()
2373        .get(http::header::CONTENT_LENGTH)
2374        .and_then(|header_value| header_value.to_str().ok())
2375        .and_then(|s| s.parse::<usize>().ok())
2376    {
2377        // For compressed content, we pre-allocate a multiple of the
2378        // compressed size, assuming typical content will be highly compressed.
2379        let multiplier: usize = if response_stream.body().is_encoded() {
2380            5
2381        } else {
2382            1
2383        };
2384        possible_length.saturating_mul(multiplier)
2385    } else {
2386        // We don't know the length, so we fallback to something to still
2387        // avoid some reallocs.
2388        4096
2389    }
2390    .min(pref!(network_max_content_length) as usize);
2391    let _ = done_sender.send(Data::ContentLength(prealloc_size));
2392    *response_body.lock() = ResponseBody::Receiving(Vec::with_capacity(prealloc_size));
2393    let response_body2 = response_body.clone();
2394
2395    spawn_task(
2396        response_stream
2397            .into_body()
2398            .try_fold(response_body, move |response_body_accumulator, chunk| {
2399                if cancellation_listener.cancelled() {
2400                    *response_body_accumulator.lock() = ResponseBody::Done(vec![]);
2401                    let _ = done_sender.send(Data::Cancelled);
2402                    return future::ready(Err(std::io::Error::new(
2403                        std::io::ErrorKind::Interrupted,
2404                        "Fetch aborted",
2405                    )));
2406                }
2407                if let ResponseBody::Receiving(ref mut body) = *response_body_accumulator.lock() {
2408                    body.extend_from_slice(&chunk);
2409                    let _ = done_sender.send(Data::Payload(chunk));
2410                }
2411                future::ready(Ok(response_body_accumulator))
2412            })
2413            .and_then(move |complete_response_body| {
2414                debug!("successfully finished response for {:?}", url1);
2415                let mut body = complete_response_body.lock();
2416                let mut completed_body = match *body {
2417                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2418                    _ => vec![],
2419                };
2420                // This allocation may be retained by the http-cache.
2421                completed_body.shrink_to_fit();
2422                // If devtools is disabled avoid cloning, since the result would
2423                // be unused anyway.
2424                #[cfg(feature = "devtools")]
2425                let devtools_response_body =
2426                    devtools_chan.is_some().then(|| completed_body.clone());
2427                *body = ResponseBody::Done(completed_body);
2428                #[cfg(feature = "devtools")]
2429                send_response_values_to_devtools(
2430                    Some(headers),
2431                    status,
2432                    devtools_response_body,
2433                    CacheState::None,
2434                    &devtools_request,
2435                    devtools_chan,
2436                );
2437                timing_ptr2.set_attribute(ResourceAttribute::ResponseEnd);
2438                let _ = done_sender2.send(Data::Done);
2439                future::ready(Ok(()))
2440            })
2441            .map_err(move |error| {
2442                if let std::io::ErrorKind::InvalidData = error.kind() {
2443                    debug!("Content decompression error for {:?}", url2);
2444                    let _ = done_sender3.send(Data::Error(NetworkError::DecompressionError));
2445                    let mut body = response_body2.lock();
2446
2447                    *body = ResponseBody::Done(vec![]);
2448                }
2449                debug!("finished response for {:?}", url2);
2450                let mut body = response_body2.lock();
2451                let completed_body = match *body {
2452                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2453                    _ => vec![],
2454                };
2455                *body = ResponseBody::Done(completed_body);
2456                timing_ptr3.set_attribute(ResourceAttribute::ResponseEnd);
2457                let _ = done_sender3.send(Data::Done);
2458            }),
2459    );
2460
2461    // TODO these substeps aren't possible yet
2462    // Substep 1
2463
2464    // Substep 2
2465
2466    // TODO Read request
2467
2468    // Step 6-11
2469    // (needs stream bodies)
2470
2471    // Step 13
2472    // TODO this step isn't possible yet (CSP)
2473
2474    // Step 14, update the cached response, done via the shared response body.
2475
2476    // TODO this step isn't possible yet
2477    // Step 15
2478    if credentials_flag {
2479        set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar);
2480    }
2481    context
2482        .state
2483        .hsts_list
2484        .write()
2485        .update_hsts_list_from_response(&url, &response.headers);
2486
2487    // TODO these steps
2488    // Step 16
2489    // Substep 1
2490    // Substep 2
2491    // Sub-substep 1
2492    // Sub-substep 2
2493    // Sub-substep 3
2494    // Sub-substep 4
2495    // Substep 3
2496
2497    // Step 16
2498
2499    // Ensure we don't override "responseEnd" on successful return of this function
2500    response_end_timer.neuter();
2501    response
2502}
2503
2504/// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch)
2505async fn cors_preflight_fetch(request: &Request, context: &FetchContext) -> Response {
2506    // Step 1. Let preflight be a new request whose method is `OPTIONS`, URL list is a clone
2507    // of request’s URL list, initiator is request’s initiator, destination is request’s destination,
2508    // origin is request’s origin, referrer is request’s referrer, referrer policy is request’s
2509    // referrer policy, mode is "cors", and response tainting is "cors".
2510    let mut preflight = RequestBuilder::new(
2511        request.target_webview_id,
2512        request.current_url_with_blob_claim(),
2513        request.referrer.clone(),
2514    )
2515    .method(Method::OPTIONS)
2516    .origin(match &request.origin {
2517        Origin::Client => {
2518            unreachable!("We shouldn't get Client origin in cors_preflight_fetch.")
2519        },
2520        Origin::Origin(origin) => origin.clone(),
2521    })
2522    .pipeline_id(request.pipeline_id)
2523    .initiator(request.initiator)
2524    .destination(request.destination)
2525    .referrer_policy(request.referrer_policy)
2526    .mode(RequestMode::CorsMode)
2527    .response_tainting(ResponseTainting::CorsTainting)
2528    .policy_container(match &request.policy_container {
2529        RequestPolicyContainer::Client => {
2530            unreachable!("We should have a policy container for request in cors_preflight_fetch")
2531        },
2532        RequestPolicyContainer::PolicyContainer(policy_container) => policy_container.clone(),
2533    })
2534    .url_list(request.url_list.clone())
2535    .build();
2536
2537    // Step 2. Append (`Accept`, `*/*`) to preflight’s header list.
2538    preflight
2539        .headers
2540        .insert(ACCEPT, HeaderValue::from_static("*/*"));
2541
2542    // Step 3. Append (`Access-Control-Request-Method`, request’s method) to preflight’s header list.
2543    preflight
2544        .headers
2545        .typed_insert::<AccessControlRequestMethod>(AccessControlRequestMethod::from(
2546            request.method.clone(),
2547        ));
2548
2549    // Step 4. Let headers be the CORS-unsafe request-header names with request’s header list.
2550    let headers = get_cors_unsafe_header_names(&request.headers);
2551
2552    // Step 5 If headers is not empty, then:
2553    if !headers.is_empty() {
2554        // 5.1 Let value be the items in headers separated from each other by `,`
2555        // TODO(36451): replace this with typed_insert when headers fixes headers#207
2556        preflight.headers.insert(
2557            ACCESS_CONTROL_REQUEST_HEADERS,
2558            HeaderValue::from_bytes(itertools::join(headers.iter(), ",").as_bytes())
2559                .unwrap_or(HeaderValue::from_static("")),
2560        );
2561    }
2562
2563    // Step 6. Let response be the result of running HTTP-network-or-cache fetch given a
2564    // new fetch params whose request is preflight.
2565    let mut fetch_params = FetchParams::new(
2566        preflight.clone(),
2567        ResourceFetchTiming::new(ResourceTimingType::None).into(),
2568    );
2569
2570    send_early_httprequest_to_devtools(&preflight, context);
2571
2572    let response =
2573        http_network_or_cache_fetch(&mut fetch_params, false, false, &mut None, context).await;
2574
2575    send_response_to_devtools(&preflight, context, &response, None);
2576    send_security_info_to_devtools(&preflight, context, &response);
2577
2578    // Step 7. If a CORS check for request and response returns success and response’s status is an ok status, then:
2579    if cors_check(request, &response).is_ok() && response.status.code().is_success() {
2580        // Step 7.1 Let methods be the result of extracting header list values given
2581        // `Access-Control-Allow-Methods` and response’s header list.
2582        let mut methods = if response
2583            .headers
2584            .contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)
2585        {
2586            match response.headers.typed_get::<AccessControlAllowMethods>() {
2587                Some(methods) => methods.iter().collect(),
2588                // Step 7.3 If either methods or headerNames is failure, return a network error.
2589                None => {
2590                    return Response::network_error(NetworkError::CorsAllowMethods);
2591                },
2592            }
2593        } else {
2594            vec![]
2595        };
2596
2597        // Step 7.2 Let headerNames be the result of extracting header list values given
2598        // `Access-Control-Allow-Headers` and response’s header list.
2599        let header_names = if response
2600            .headers
2601            .contains_key(header::ACCESS_CONTROL_ALLOW_HEADERS)
2602        {
2603            match response.headers.typed_get::<AccessControlAllowHeaders>() {
2604                Some(names) => names.iter().collect(),
2605                // Step 7.3 If either methods or headerNames is failure, return a network error.
2606                None => {
2607                    return Response::network_error(NetworkError::CorsAllowHeaders);
2608                },
2609            }
2610        } else {
2611            vec![]
2612        };
2613
2614        debug!(
2615            "CORS check: Allowed methods: {:?}, current method: {:?}",
2616            methods, request.method
2617        );
2618
2619        // Step 7.4 If methods is null and request’s use-CORS-preflight flag is set,
2620        // then set methods to a new list containing request’s method.
2621        if methods.is_empty() && request.use_cors_preflight {
2622            methods = vec![request.method.clone()];
2623        }
2624
2625        // Step 7.5 If request’s method is not in methods, request’s method is not a CORS-safelisted method,
2626        // and request’s credentials mode is "include" or methods does not contain `*`, then return a network error.
2627        if methods
2628            .iter()
2629            .all(|method| *method.as_str() != *request.method.as_ref()) &&
2630            !is_cors_safelisted_method(&request.method) &&
2631            (request.credentials_mode == CredentialsMode::Include ||
2632                methods.iter().all(|method| method.as_ref() != "*"))
2633        {
2634            return Response::network_error(NetworkError::CorsMethod);
2635        }
2636
2637        debug!(
2638            "CORS check: Allowed headers: {:?}, current headers: {:?}",
2639            header_names, request.headers
2640        );
2641
2642        // Step 7.6 If one of request’s header list’s names is a CORS non-wildcard request-header name
2643        // and is not a byte-case-insensitive match for an item in headerNames, then return a network error.
2644        //
2645        // Note: This check deviates from the spec. Other browsers (Chrome, Firefox, Safari) all treat a
2646        // `*` in headerNames as covering CORS non-wildcard request-header names.
2647        let header_names_set: HashSet<&HeaderName> = HashSet::from_iter(header_names.iter());
2648        if request.headers.iter().any(|(name, _)| {
2649            is_cors_non_wildcard_request_header_name(name) &&
2650                !header_names_set.contains(name) &&
2651                !header_names_set.contains(&HeaderName::from_static("*"))
2652        }) {
2653            return Response::network_error(NetworkError::CorsAuthorization);
2654        }
2655
2656        // Step 7.7 For each unsafeName of the CORS-unsafe request-header names with request’s header list,
2657        // if unsafeName is not a byte-case-insensitive match for an item in headerNames and request’s credentials
2658        // mode is "include" or headerNames does not contain `*`, return a network error.
2659        let unsafe_names = get_cors_unsafe_header_names(&request.headers);
2660        for unsafe_name in unsafe_names.iter() {
2661            if !header_names_set.contains(unsafe_name) &&
2662                (request.credentials_mode == CredentialsMode::Include ||
2663                    !header_names_set.contains(&HeaderName::from_static("*")))
2664            {
2665                return Response::network_error(NetworkError::CorsHeaders);
2666            }
2667        }
2668
2669        // Step 7.8 Let max-age be the result of extracting header list values given
2670        // `Access-Control-Max-Age` and response’s header list.
2671        let max_age: Option<Duration> = response
2672            .headers
2673            .typed_get::<AccessControlMaxAge>()
2674            .map(|acma| acma.into());
2675
2676        // Step 7.9 If max-age is failure or null, then set max-age to 5.
2677        let max_age = max_age.unwrap_or(Duration::from_secs(5));
2678
2679        // Step 7.10 If max-age is greater than an imposed limit on max-age, then set max-age to the imposed limit.
2680        // TODO: Need to define what an imposed limit on max-age is
2681
2682        // Step 7.11 If the user agent does not provide for a cache, then return response.
2683        // NOTE: This can be ignored, we do have a CORS cache
2684
2685        {
2686            let mut cors_cache = context.state.cors_cache.write();
2687            // Step 7.12 For each method in methods for which there is a method cache entry match using request,
2688            // set matching entry’s max-age to max-age.
2689            // Step 7.13 For each method in methods for which there is no method cache entry match using request,
2690            // create a new cache entry with request, max-age, method, and null.
2691            for method in &methods {
2692                cors_cache.match_method_and_update(request, method, max_age);
2693            }
2694
2695            // Step 7.14 For each headerName in headerNames for which there is a header-name cache entry match using request,
2696            // set matching entry’s max-age to max-age.
2697            // Step 7.15 For each headerName in headerNames for which there is no header-name cache entry match using request,
2698            // create a new cache entry with request, max-age, null, and headerName.
2699            for header_name in &header_names {
2700                cors_cache.match_header_and_update(request, header_name, max_age);
2701            }
2702        }
2703
2704        // Step 7.16 Return response.
2705        return response;
2706    }
2707
2708    // Step 8 Return a network error.
2709    Response::network_error(NetworkError::CorsGeneral)
2710}
2711
2712/// [CORS check](https://fetch.spec.whatwg.org#concept-cors-check)
2713fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
2714    // Step 1. Let origin be the result of getting `Access-Control-Allow-Origin` from response’s header list.
2715    let Some(origins) =
2716        get_value_from_header_list(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), &response.headers)
2717    else {
2718        // Step 2. If origin is null, then return failure.
2719        return Err(());
2720    };
2721    let origin = String::from_utf8(origins).map_err(|_| ())?;
2722
2723    // Step 3. If request’s credentials mode is not "include" and origin is `*`, then return success.
2724    if request.credentials_mode != CredentialsMode::Include && origin == "*" {
2725        return Ok(());
2726    }
2727
2728    // Step 4. If the result of byte-serializing a request origin with request is not origin, then return failure.
2729    if serialize_request_origin(request).to_string() != origin {
2730        return Err(());
2731    }
2732
2733    // Step 5. If request’s credentials mode is not "include", then return success.
2734    if request.credentials_mode != CredentialsMode::Include {
2735        return Ok(());
2736    }
2737
2738    // Step 6. Let credentials be the result of getting `Access-Control-Allow-Credentials` from response’s header list.
2739    let credentials = response
2740        .headers
2741        .typed_get::<AccessControlAllowCredentials>();
2742
2743    // Step 7. If credentials is `true`, then return success.
2744    if credentials.is_some() {
2745        return Ok(());
2746    }
2747
2748    // Step 8. Return failure.
2749    Err(())
2750}
2751
2752fn has_credentials(url: &ServoUrl) -> bool {
2753    !url.username().is_empty() || url.password().is_some()
2754}
2755
2756fn is_no_store_cache(headers: &HeaderMap) -> bool {
2757    headers.contains_key(header::IF_MODIFIED_SINCE) |
2758        headers.contains_key(header::IF_NONE_MATCH) |
2759        headers.contains_key(header::IF_UNMODIFIED_SINCE) |
2760        headers.contains_key(header::IF_MATCH) |
2761        headers.contains_key(header::IF_RANGE)
2762}
2763
2764/// <https://fetch.spec.whatwg.org/#redirect-status>
2765fn is_redirect_status(status: StatusCode) -> bool {
2766    matches!(
2767        status,
2768        StatusCode::MOVED_PERMANENTLY |
2769            StatusCode::FOUND |
2770            StatusCode::SEE_OTHER |
2771            StatusCode::TEMPORARY_REDIRECT |
2772            StatusCode::PERMANENT_REDIRECT
2773    )
2774}
2775
2776/// <https://fetch.spec.whatwg.org/#serializing-a-request-origin>
2777fn serialize_request_origin(request: &Request) -> headers::Origin {
2778    // Step 1. Assert: request’s origin is not "client".
2779    let Origin::Origin(origin) = &request.origin else {
2780        panic!("origin cannot be \"client\" at this point in time");
2781    };
2782
2783    // Step 2. If request’s redirect-taint is not "same-origin", then return "null".
2784    if request.redirect_taint_for_request() != RedirectTaint::SameOrigin {
2785        return headers::Origin::NULL;
2786    }
2787
2788    // Step 3. Return request’s origin, serialized.
2789    serialize_origin(origin)
2790}
2791
2792/// Step 3 of <https://fetch.spec.whatwg.org/#serializing-a-request-origin>.
2793pub fn serialize_origin(origin: &ImmutableOrigin) -> headers::Origin {
2794    match origin {
2795        ImmutableOrigin::Opaque(_) => headers::Origin::NULL,
2796        ImmutableOrigin::Tuple(scheme, host, port) => {
2797            // Note: This must be kept in sync with `Origin::ascii_serialization()`, which does not
2798            // use the port number when a default port is used.
2799            let port = match (scheme.as_ref(), port) {
2800                ("http" | "ws", 80) | ("https" | "wss", 443) | ("ftp", 21) => None,
2801                _ => Some(*port),
2802            };
2803
2804            // TODO: Ensure that hyper/servo don't disagree about valid origin headers
2805            headers::Origin::try_from_parts(scheme, &host.to_string(), port)
2806                .unwrap_or(headers::Origin::NULL)
2807        },
2808    }
2809}
2810
2811/// <https://fetch.spec.whatwg.org/#append-a-request-origin-header>
2812#[expect(
2813    clippy::collapsible_match,
2814    reason = "The current way follows the spec more closely"
2815)]
2816fn append_a_request_origin_header(request: &mut Request) {
2817    // Step 1. Assert: request’s origin is not "client".
2818    let Origin::Origin(request_origin) = &request.origin else {
2819        panic!("origin cannot be \"client\" at this point in time");
2820    };
2821
2822    // Step 2. Let serializedOrigin be the result of byte-serializing a request origin with request.
2823    let mut serialized_origin = serialize_request_origin(request);
2824
2825    // Step 3. If request’s response tainting is "cors" or request’s mode is "websocket",
2826    //         then append (`Origin`, serializedOrigin) to request’s header list.
2827    if request.response_tainting == ResponseTainting::CorsTainting ||
2828        matches!(request.mode, RequestMode::WebSocket { .. })
2829    {
2830        request.headers.typed_insert(serialized_origin);
2831    }
2832    // Step 4. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
2833    else if !matches!(request.method, Method::GET | Method::HEAD) {
2834        // Step 4.1 If request’s mode is not "cors", then switch on request’s referrer policy:
2835        if request.mode != RequestMode::CorsMode {
2836            match request.referrer_policy {
2837                ReferrerPolicy::NoReferrer => {
2838                    // Set serializedOrigin to `null`.
2839                    serialized_origin = headers::Origin::NULL;
2840                },
2841                ReferrerPolicy::NoReferrerWhenDowngrade |
2842                ReferrerPolicy::StrictOrigin |
2843                ReferrerPolicy::StrictOriginWhenCrossOrigin => {
2844                    // If request’s origin is a tuple origin, its scheme is "https", and
2845                    // request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
2846                    if let ImmutableOrigin::Tuple(scheme, _, _) = &request_origin &&
2847                        scheme == "https" &&
2848                        request.current_url().scheme() != "https"
2849                    {
2850                        serialized_origin = headers::Origin::NULL;
2851                    }
2852                },
2853                ReferrerPolicy::SameOrigin => {
2854                    // If request’s origin is not same origin with request’s current URL’s origin,
2855                    // then set serializedOrigin to `null`.
2856                    if *request_origin != request.current_url().origin() {
2857                        serialized_origin = headers::Origin::NULL;
2858                    }
2859                },
2860                _ => {
2861                    // Otherwise, do nothing.
2862                },
2863            };
2864        }
2865
2866        // Step 4.2. Append (`Origin`, serializedOrigin) to request’s header list.
2867        request.headers.typed_insert(serialized_origin);
2868    }
2869}
2870
2871/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request>
2872fn append_the_fetch_metadata_headers(r: &mut Request) {
2873    // Step 1. If r’s current url is not an potentially trustworthy URL, return.
2874    if !r.current_url().is_potentially_trustworthy() {
2875        return;
2876    }
2877
2878    // Step 2. Set the Sec-Fetch-Dest header for r.
2879    set_the_sec_fetch_dest_header(r);
2880
2881    // Step 3. Set the Sec-Fetch-Mode header for r.
2882    set_the_sec_fetch_mode_header(r);
2883
2884    // Step 4. Set the Sec-Fetch-Site header for r.
2885    set_the_sec_fetch_site_header(r);
2886
2887    // Step 5. Set the Sec-Fetch-User header for r.
2888    set_the_sec_fetch_user_header(r);
2889}
2890
2891/// Steps 8.16 to 8.18 in [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
2892fn append_cache_data_to_headers(http_request: &mut Request) {
2893    match http_request.cache_mode {
2894        // Step 8.16: If httpRequest’s cache mode is "default" and httpRequest’s header list
2895        // contains `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or
2896        // `If-Range`, then set httpRequest’s cache mode to "no-store".
2897        CacheMode::Default if is_no_store_cache(&http_request.headers) => {
2898            http_request.cache_mode = CacheMode::NoStore;
2899        },
2900
2901        // Note that the following steps (8.17 and 8.18) are being considered for removal:
2902        // https://github.com/whatwg/fetch/issues/722#issuecomment-1420264615
2903
2904        // Step 8.17: If httpRequest’s cache mode is "no-cache", httpRequest’s prevent no-cache
2905        // cache-control header modification flag is unset, and httpRequest’s header list does not
2906        // contain `Cache-Control`, then append (`Cache-Control`, `max-age=0`) to httpRequest’s
2907        // header list.
2908        // TODO: Implement request's prevent no-cache cache-control header modification flag
2909        // https://fetch.spec.whatwg.org/#no-cache-prevent-cache-control
2910        CacheMode::NoCache if !http_request.headers.contains_key(header::CACHE_CONTROL) => {
2911            http_request
2912                .headers
2913                .typed_insert(CacheControl::new().with_max_age(Duration::from_secs(0)));
2914        },
2915
2916        // Step 8.18: If httpRequest’s cache mode is "no-store" or "reload", then:
2917        CacheMode::Reload | CacheMode::NoStore => {
2918            // Step 8.18.1: If httpRequest’s header list does not contain `Pragma`, then append
2919            // (`Pragma`, `no-cache`) to httpRequest’s header list.
2920            if !http_request.headers.contains_key(header::PRAGMA) {
2921                http_request.headers.typed_insert(Pragma::no_cache());
2922            }
2923
2924            // Step 8.18.2: If httpRequest’s header list does not contain `Cache-Control`, then
2925            // append (`Cache-Control`, `no-cache`) to httpRequest’s header list.
2926            if !http_request.headers.contains_key(header::CACHE_CONTROL) {
2927                http_request
2928                    .headers
2929                    .typed_insert(CacheControl::new().with_no_cache());
2930            }
2931        },
2932
2933        _ => {},
2934    }
2935}
2936
2937/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest>
2938fn set_the_sec_fetch_dest_header(r: &mut Request) {
2939    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2940    debug_assert!(r.current_url().is_potentially_trustworthy());
2941
2942    // Step 2. Let header be a Structured Header whose value is a token.
2943    // Step 3. If r’s destination is the empty string, set header’s value to the string "empty".
2944    // Otherwise, set header’s value to r’s destination.
2945    let header = r.destination;
2946
2947    // Step 4. Set a structured field value `Sec-Fetch-Dest`/header in r’s header list.
2948    r.headers.typed_insert(SecFetchDest(header));
2949}
2950
2951/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-mode>
2952fn set_the_sec_fetch_mode_header(r: &mut Request) {
2953    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2954    debug_assert!(r.current_url().is_potentially_trustworthy());
2955
2956    // Step 2. Let header be a Structured Header whose value is a token.
2957    // Step 3. Set header’s value to r’s mode.
2958    let header = &r.mode;
2959
2960    // Step 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
2961    r.headers.typed_insert(SecFetchMode::from(header));
2962}
2963
2964/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site>
2965fn set_the_sec_fetch_site_header(r: &mut Request) {
2966    // The webappsec spec seems to have a similar issue as
2967    // https://github.com/whatwg/fetch/issues/1773
2968    let Origin::Origin(request_origin) = &r.origin else {
2969        panic!("request origin cannot be \"client\" at this point")
2970    };
2971
2972    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2973    debug_assert!(r.current_url().is_potentially_trustworthy());
2974
2975    // Step 2. Let header be a Structured Header whose value is a token.
2976    // Step 3. Set header’s value to same-origin.
2977    let mut header = SecFetchSite::SameOrigin;
2978
2979    // TODO: Step 3. If r is a navigation request that was explicitly caused by a
2980    // user’s interaction with the user agent, then set header’s value to none.
2981
2982    // Step 5. If header’s value is not none, then for each url in r’s url list:
2983    if header != SecFetchSite::None {
2984        for url in &r.url_list {
2985            // Step 5.1 If url is same origin with r’s origin, continue.
2986            if url.origin() == *request_origin {
2987                continue;
2988            }
2989
2990            // Step 5.2 Set header’s value to cross-site.
2991            header = SecFetchSite::CrossSite;
2992
2993            // Step 5.3 If r’s origin is not same site with url’s origin, then break.
2994            if !is_same_site(request_origin, &url.origin()) {
2995                break;
2996            }
2997
2998            // Step 5.4 Set header’s value to same-site.
2999            header = SecFetchSite::SameSite;
3000        }
3001    }
3002
3003    // Step 6. Set a structured field value `Sec-Fetch-Site`/header in r’s header list.
3004    r.headers.typed_insert(header);
3005}
3006
3007/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user>
3008fn set_the_sec_fetch_user_header(r: &mut Request) {
3009    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
3010    debug_assert!(r.current_url().is_potentially_trustworthy());
3011
3012    // Step 2. If r is not a navigation request, or if r’s user-activation is false, return.
3013    if !r.is_navigation_request() || !r.user_activation {
3014        return;
3015    }
3016
3017    // Step 3. Let header be a Structured Header whose value is a token.
3018    // Step 4. Set header’s value to true.
3019    let header = SecFetchUser;
3020
3021    // Step 5. Set a structured field value `Sec-Fetch-User`/header in r’s header list.
3022    r.headers.typed_insert(header);
3023}
3024
3025/// <https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect>
3026fn set_requests_referrer_policy_on_redirect(request: &mut Request, response: &Response) {
3027    // Step 1: Let policy be the result of executing § 8.1 Parse a referrer policy from a
3028    // Referrer-Policy header on actualResponse.
3029    let referrer_policy: ReferrerPolicy = response
3030        .headers
3031        .typed_get::<headers::ReferrerPolicy>()
3032        .into();
3033
3034    // Step 2: If policy is not the empty string, then set request’s referrer policy to policy.
3035    if referrer_policy != ReferrerPolicy::EmptyString {
3036        request.referrer_policy = referrer_policy;
3037    }
3038}