diff --git a/core/runtime/Cargo.toml b/core/runtime/Cargo.toml index dd75e661be8..ef631ca5a69 100644 --- a/core/runtime/Cargo.toml +++ b/core/runtime/Cargo.toml @@ -53,6 +53,7 @@ fetch = [ "dep:either", "dep:http", "dep:serde_json", + "dep:url", "boa_engine/either", ] reqwest-blocking = ["dep:reqwest", "reqwest/blocking"] diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index 442ed64d5eb..d4577913ca3 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -303,9 +303,9 @@ impl JsResponse { if !matches!(status, 301 | 302 | 303 | 307 | 308) { return Err(js_error!(RangeError: "Invalid redirect status: {}", status)); } - let url_str = url.to_string(context)?.to_std_string_escaped(); - http::Uri::try_from(url_str.as_str()) - .map_err(|_| js_error!(TypeError: "Invalid URL: {}", url_str))?; + let parsed_url = url::Url::parse(&url.to_string(context)?.to_std_string_escaped()) + .map_err(|e| js_error!(TypeError: "Invalid URL: {}", e))?; + let serialized_url = parsed_url.to_string(); let status_code = StatusCode::from_u16(status) .map_err(|_| js_error!(RangeError: "Invalid status code: {}", status))?; @@ -313,7 +313,7 @@ impl JsResponse { let mut headers = http::header::HeaderMap::new(); headers.insert( HeaderName::from_static("location"), - HeaderValue::try_from(url_str) + HeaderValue::try_from(serialized_url) .map_err(|_| js_error!(TypeError: "Invalid URL for header value"))?, ); diff --git a/core/runtime/src/fetch/tests/response.rs b/core/runtime/src/fetch/tests/response.rs index ee8108751d8..f6dc158618d 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -180,10 +180,74 @@ fn response_redirect_custom_status_and_coercion() { r#" const response = Response.redirect("http://example.com/", 301); assertEq(response.status, 301); + "#, + ), + ]); +} + +#[test] +fn response_redirect_rejects_non_url_after_string_coercion() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + let threw = false; + try { + Response.redirect(12345); + } catch (e) { + threw = true; + if (!(e instanceof TypeError)) { + throw new Error("Expected TypeError, got " + e.name); + } + } + if (!threw) { + throw new Error("Expected TypeError, but no error was thrown"); + } + "#, + ), + ]); +} + +#[test] +fn response_redirect_serializes_parsed_url() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + const response1 = Response.redirect("https://example.com"); + assertEq(response1.headers.get("location"), "https://example.com/"); + "#, + ), + TestAction::run( + r#" + const response2 = Response.redirect("https://example.com#frag"); + assertEq(response2.headers.get("location"), "https://example.com/#frag"); + "#, + ), + ]); +} - // Tests Web IDL coercion of the URL parameter - const response2 = Response.redirect(12345); - assertEq(response2.headers.get("location"), "12345"); +#[test] +fn response_redirect_rejects_relative_url() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + let threw = false; + try { + Response.redirect("/foo"); + } catch (e) { + threw = true; + if (!(e instanceof TypeError)) { + throw new Error("Expected TypeError, got " + e.name); + } + } + if (!threw) { + throw new Error("Expected TypeError, but no error was thrown"); + } "#, ), ]);