diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index c09bd0b999a..6a1fd7ae519 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -13,6 +13,7 @@ use boa_engine::{ js_string, }; use boa_gc::{Finalize, Trace}; +use http::{HeaderName, HeaderValue, StatusCode}; use std::rc::Rc; /// The type read-only property of the Response interface contains the type of the @@ -295,6 +296,37 @@ impl JsResponse { Self::error() } + /// `Response.redirect(url, status)` per Fetch spec ยง7.4. + #[boa(static)] + fn redirect(url: JsValue, status: Option, context: &mut Context) -> JsResult { + let status = status.unwrap_or(302); + 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 status_code = StatusCode::from_u16(status) + .map_err(|_| js_error!(RangeError: "Invalid status code: {}", status))?; + + let mut headers = http::header::HeaderMap::new(); + headers.insert( + HeaderName::from_static("location"), + HeaderValue::try_from(url_str) + .map_err(|_| js_error!(TypeError: "Invalid URL for header value"))?, + ); + + Ok(Self { + url: js_string!(""), + r#type: ResponseType::Basic, + status: status_code.as_u16(), + status_text: JsString::from(status_code.canonical_reason().unwrap_or("")), + headers: JsHeaders::from_http(headers), + body: Rc::new(Vec::new()), + }) + } + /// Creates a `Response` with a JSON-serialized body and `Content-Type: application/json`. /// /// See diff --git a/core/runtime/src/fetch/tests/response.rs b/core/runtime/src/fetch/tests/response.rs index 7d728c1f0b5..da829ecc7a3 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -156,6 +156,86 @@ fn response_getter() { ]); } +#[test] +fn response_redirect_default_status() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + const response = Response.redirect("http://example.com/"); + assertEq(response.status, 302); + assertEq(response.headers.get("location"), "http://example.com/"); + "#, + ), + ]); +} + +#[test] +fn response_redirect_custom_status_and_coercion() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + const response = Response.redirect("http://example.com/", 301); + assertEq(response.status, 301); + + // Tests Web IDL coercion of the URL parameter + const response2 = Response.redirect(12345); + assertEq(response2.headers.get("location"), "12345"); + "#, + ), + ]); +} + +#[test] +fn response_redirect_invalid_status() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + let threw = false; + try { + Response.redirect("http://example.com/", 200); + } catch (e) { + threw = true; + if (!(e instanceof RangeError)) { + throw new Error("Expected RangeError, got " + e.name); + } + } + if (!threw) { + throw new Error("Expected RangeError, but no error was thrown"); + } + "#, + ), + ]); +} + +#[test] +fn response_json_static() { + run_test_actions([ + TestAction::harness(), + TestAction::inspect_context(|ctx| register(&[], ctx)), + TestAction::run( + r#" + globalThis.p = (async () => { + const response = Response.json({ hello: "world" }); + assertEq(response.status, 200); + assertEq(response.headers.get("content-type"), "application/json"); + const body = await response.json(); + assertEq(body.hello, "world"); + })(); + "#, + ), + TestAction::inspect_context(|ctx| { + let p = ctx.global_object().get(js_str!("p"), ctx).unwrap(); + p.as_promise().unwrap().await_blocking(ctx).unwrap(); + }), + ]); +} + #[test] fn response_headers_get_combines_duplicate_values_with_comma_space() { run_test_actions([