Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/runtime/src/fetch/fetchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ impl Fetcher for BlockingReqwestFetcher {
_context: &RefCell<&mut Context>,
) -> JsResult<JsResponse> {
use boa_engine::{JsError, JsString};
use http::response;

if let Some(ref sig) = signal
&& let Some(sig_ref) = sig.downcast_ref::<crate::abort::JsAbortSignal>()
Expand Down Expand Up @@ -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());

Expand All @@ -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))
}
}
7 changes: 5 additions & 2 deletions core/runtime/src/fetch/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -155,6 +155,7 @@ impl JsResponse {

Self {
url,
redirected,
r#type: ResponseType::Basic,
status,
status_text,
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does your change align with the TODO comment and spec?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO was about tracking every hop in the redirect chain, but Response.redirected only needs to know if there was any redirect, not the full list. Since reqwest already follows redirects and exposes the final URL, comparing that to the original request URL gives us exactly what we need. Full URL list tracking could still be a follow-up if needed for other spec requirements.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response.redirected only needs to know if there was any redirect, not the full list

But according to the comment here, spec requires the check to be done via url list, and not a "quick" check like this?

E.g. what if A redirects to B and B redirects back to A? Does your check consider scenario like this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that!
You're right, A→B→A would break my check. What approach would you recommend, manual redirect handling to build the full URL list, or something simpler?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the response only has url, but according to spec and the TODO comment, url list is also required: https://fetch.spec.whatwg.org/#concept-response-url-list . So i guess you should implement that and follow the spec for where to modify it in what way.

But fetch api is in general not complete in boa, so idk how much work that actually might be

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing once you have the list, then A -> B -> A would be handled correctly because of the url list containing multiple urls even though the resulting url might be the same as the original.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, we need to keep a list of urls as described in the specification, since we are using reqwest we can create a client that has custom redirect policy, that will collect the url attempts.

    // part of fetcher initialization:
    let url_list = Arc::new(Mutex::new(Vec::new()));
    let url_list_clone = Arc::clone(&history);

    let custom_policy = redirect::Policy::custom(move |attempt| {
        url_list_clone.lock().expect("should not be poisoned").push(attempt.url().clone());
        reqwest::redirect::Policy::default().redirect(attempt)
    });

    let client = Client::builder()
        .redirect(custom_policy)
        .build()?;

    // making the request
    let result = self.client.execute(req);
    
    // clears and takes the list for the previous request.
    let url_list = std::mem::take(&mut *self.url_list.lock().expect("should not be poisoned"));

    let response = result.map_err(JsError::from_rust)?;

}

#[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(),
Expand Down
32 changes: 32 additions & 0 deletions core/runtime/src/fetch/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ impl crate::fetch::Fetcher for E2eFetcher {
) -> JsResult<JsResponse> {
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.")),
}
}
Expand Down Expand Up @@ -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),
]);
}
16 changes: 16 additions & 0 deletions core/runtime/src/fetch/tests/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}),
// ...
]);
}
Loading