diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 8bc519f402e..d1e5abc0eba 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -27,7 +27,7 @@ use headers::{ }; use http::header::{ self, ACCEPT, ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION, CONTENT_ENCODING, - CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE, + CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE, WWW_AUTHENTICATE, }; use http::{HeaderMap, Method, Request as HyperRequest, StatusCode}; use http_body_util::combinators::BoxBody; @@ -1719,8 +1719,12 @@ async fn http_network_or_cache_fetch( // Step 14. If response’s status is 401, httpRequest’s response tainting is not "cors", // includeCredentials is true, and request’s window is an environment settings object, then: // TODO(#33616): Figure out what to do with request window objects - if let (Some(StatusCode::UNAUTHORIZED), false, true) = - (response.status.try_code(), cors_flag, include_credentials) + // NOTE: Requiring a WWW-Authenticate header here is ad-hoc, but seems to match what other browsers are + // doing. See Step 14.1. + if response.status.try_code() == Some(StatusCode::UNAUTHORIZED) && + !cors_flag && + include_credentials && + response.headers.contains_key(WWW_AUTHENTICATE) { // TODO: Step 14.1 Spec says requires testing on multiple WWW-Authenticate headers diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index 62ee090225d..54fdfc1d81f 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -25,7 +25,7 @@ use headers::authorization::Basic; use headers::{ Authorization, ContentLength, Date, HeaderMapExt, Host, StrictTransportSecurity, UserAgent, }; -use http::header::{self, HeaderMap, HeaderValue}; +use http::header::{self, HeaderMap, HeaderValue, WWW_AUTHENTICATE}; use http::uri::Authority; use http::{HeaderName, Method, StatusCode}; use http_body_util::combinators::BoxBody; @@ -1717,6 +1717,10 @@ fn test_prompt_credentials_when_client_receives_unauthorized_response() { } else { *response.status_mut() = StatusCode::UNAUTHORIZED; } + + response + .headers_mut() + .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Basic")); }; let (server, url) = make_server(handler); @@ -1744,16 +1748,71 @@ fn test_prompt_credentials_when_client_receives_unauthorized_response() { server.close(); - assert!( - response - .internal_response - .unwrap() - .status - .code() - .is_success() + assert_eq!( + response.internal_response.unwrap().status.code(), + StatusCode::OK ); } +#[test] +fn test_dont_prompt_credentials_when_unauthorized_response_contains_no_www_authenticate_header() { + let handler = + move |request: HyperRequest, + response: &mut HyperResponse>| { + assert!( + request + .headers() + .typed_get::>() + .is_none() + ); + *response.status_mut() = StatusCode::UNAUTHORIZED; + }; + + let (server, url) = make_server(handler); + + let request = RequestBuilder::new(Some(TEST_WEBVIEW_ID), url.clone(), Referrer::NoReferrer) + .method(Method::GET) + .body(None) + .destination(Destination::Document) + .mode(RequestMode::Navigate) + .origin(mock_origin()) + .pipeline_id(Some(TEST_PIPELINE_ID)) + .credentials_mode(CredentialsMode::Include) + .build(); + + let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver(); + let handle = std::thread::spawn(move || { + loop { + let Ok(msg) = embedder_receiver.recv() else { + return; + }; + match msg { + embedder_traits::EmbedderMsg::RequestAuthentication(..) => { + panic!( + "Should not have requested authentication as there's no www-authenticate header" + ); + }, + embedder_traits::EmbedderMsg::WebResourceRequested(..) => {}, + _ => unreachable!(), + } + } + }); + let mut context = new_fetch_context(None, Some(embedder_proxy), None); + + let response = fetch_with_context(request, &mut context); + + server.close(); + + assert_eq!( + response.internal_response.unwrap().status.code(), + StatusCode::UNAUTHORIZED + ); + + // Without this join we won't notice if the mock embedder thread panics! + drop(context); // Dropping this context causes the embedder thread to exit. + handle.join().unwrap(); +} + #[test] fn test_prompt_credentials_user_cancels_dialog_input() { let handler =