diff --git a/core/runtime/src/fetch/fetchers.rs b/core/runtime/src/fetch/fetchers.rs index 12f017bb5a6..8567dc77837 100644 --- a/core/runtime/src/fetch/fetchers.rs +++ b/core/runtime/src/fetch/fetchers.rs @@ -39,6 +39,7 @@ impl Fetcher for BlockingReqwestFetcher { _context: &RefCell<&mut Context>, ) -> JsResult { use boa_engine::{JsError, JsString}; + use http::response; if let Some(ref sig) = signal && let Some(sig_ref) = sig.downcast_ref::() @@ -74,6 +75,8 @@ impl Fetcher for BlockingReqwestFetcher { let status = resp.status(); let headers = resp.headers().clone(); + let final_url = resp.url().to_string(); // <-- capture before consuming + let redirected = final_url != url; let bytes = resp.bytes().map_err(JsError::from_rust)?; let mut builder = http::Response::builder().status(status.as_u16()); @@ -83,9 +86,13 @@ impl Fetcher for BlockingReqwestFetcher { } } + let final_url = resp.url().to_string(); + let redirected = final_url != url; + + builder .body(bytes.to_vec()) .map_err(JsError::from_rust) - .map(|request| JsResponse::basic(JsString::from(url), request)) + .map(|request| JsResponse::basic(JsString::from(final_url), redirected, response)) } } diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index cd13f55eba5..0598e78c324 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -123,7 +123,7 @@ fn is_valid_reason_phrase(s: &str) -> bool { #[derive(Clone, Debug, Trace, Finalize, JsData)] pub struct JsResponse { url: JsString, - + redirected: bool, #[unsafe_ignore_trace] r#type: ResponseType, @@ -155,6 +155,7 @@ impl JsResponse { Self { url, + redirected, r#type: ResponseType::Basic, status, status_text, @@ -170,6 +171,7 @@ impl JsResponse { pub fn error() -> Self { Self { url: js_string!(""), + redirected: false, r#type: ResponseType::Error, // A network error's status is always 0. // See https://fetch.spec.whatwg.org/#concept-network-error @@ -443,13 +445,14 @@ impl JsResponse { fn redirected(&self) -> bool { // The spec says: return true if this's response's URL list's size is greater than 1. // TODO: track the full URL list to implement this properly. - false + self.redirected } #[boa(rename = "clone")] fn clone_response(&self) -> Self { Self { url: self.url.clone(), + redirected: self.redirected, r#type: self.r#type, status: self.status, status_text: self.status_text.clone(), diff --git a/core/runtime/src/fetch/tests/e2e.rs b/core/runtime/src/fetch/tests/e2e.rs index 551483e781d..881484b3637 100644 --- a/core/runtime/src/fetch/tests/e2e.rs +++ b/core/runtime/src/fetch/tests/e2e.rs @@ -51,6 +51,16 @@ impl crate::fetch::Fetcher for E2eFetcher { ) -> JsResult { match request.uri().path() { "/headers" => Self::headers(&request, &mut context.borrow_mut()), + "/redirect" => Ok(JsResponse::basic( + JsString::from("http://unit.test/target"), + true, + Response::new(b"redirected body".to_vec()), + )), + "/target" => Ok(JsResponse::basic( + JsString::from("http://unit.test/target"), + false, + Response::new(b"target body".to_vec()), + )), _ => Err(js_error!("Invalid request.")), } } @@ -89,3 +99,25 @@ fn custom_header() { TestAction::inspect_context(await_response), ]); } + +#[test] +fn response_redirected_flag() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(register), + TestAction::run( + r#" + globalThis.response = (async () => { + const r = await fetch("http://unit.test/redirect"); + assertEq(r.redirected, true); + assertEq(r.url, "http://unit.test/target"); + + const r2 = await fetch("http://unit.test/target"); + assertEq(r2.redirected, false); + assertEq(r2.url, "http://unit.test/target"); + })(); + "#, + ), + TestAction::inspect_context(await_response), + ]); +} diff --git a/core/runtime/src/fetch/tests/response.rs b/core/runtime/src/fetch/tests/response.rs index c49a6d18601..2e4cdf47051 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -467,3 +467,19 @@ fn response_clone_preserves_status() { }), ]); } + +#[test] +fn response_redirected_true_after_redirect() { + // We simulate a redirect by using a custom E2e-style fetcher that + // returns a response with a different final URL. + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| { + let mut fetcher = TestFetcher::default(); + // Simulate: fetching /redirect gives a response that "came from" /target + // We need to use the redirected constructor. For now we test via E2eFetcher. + crate::fetch::register(fetcher, None, ctx).expect("register"); + }), + // ... + ]); +} \ No newline at end of file