From 0a6496570cd6da4bd1a7fe23c1f90bc097e846da Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:05:23 +0530 Subject: [PATCH 1/2] feat(runtime): add Response.redirect() and Response.json() --- core/runtime/src/fetch/response.rs | 73 ++++++++++++++++++++- core/runtime/src/fetch/tests/response.rs | 80 ++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index 324040d71c6..90f5b8b18c1 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -7,13 +7,13 @@ use crate::fetch::headers::JsHeaders; use boa_engine::object::builtins::{JsPromise, JsUint8Array}; -use boa_engine::value::{TryFromJs, TryIntoJs}; +use boa_engine::value::{Convert, TryFromJs, TryIntoJs}; use boa_engine::{ Context, JsData, JsNativeError, JsResult, JsString, JsValue, boa_class, js_error, js_str, js_string, }; use boa_gc::{Finalize, Trace}; -use http::StatusCode; +use http::{HeaderName, HeaderValue, StatusCode}; use std::rc::Rc; /// The type read-only property of the Response interface contains the type of the @@ -165,6 +165,75 @@ 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: Some(status_code), + headers: JsHeaders::from_http(headers), + body: Rc::new(Vec::new()), + }) + } + + /// `Response.json(data, init)` per Fetch spec §7.4. + #[boa(static)] + #[boa(rename = "json")] + fn json_static( + data: JsValue, + init: Option, + context: &mut Context, + ) -> JsResult { + let json_value = data + .to_json(context)? + .ok_or_else(|| js_error!(TypeError: "Cannot serialize undefined to JSON"))?; + let body = serde_json::to_string(&json_value) + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + + let status = init.as_ref().and_then(|o| o.status).unwrap_or(200); + let status_code = StatusCode::from_u16(status) + .map_err(|_| js_error!(RangeError: "Invalid status code: {}", status))?; + + let mut headers = init + .as_ref() + .and_then(|o| o.headers.clone()) + .unwrap_or_default(); + // Per spec, set Content-Type only if not already present. + if !headers.has(Convert(String::from("content-type")))? { + headers.append( + Convert(String::from("content-type")), + Convert(String::from("application/json")), + )?; + } + + Ok(Self { + url: js_string!(""), + r#type: ResponseType::Basic, + status: Some(status_code), + headers, + body: Rc::new(body.into_bytes()), + }) + } + #[boa(constructor)] fn constructor(_body: Option, _options: JsResponseOptions) -> Self { Self::basic(js_string!(""), http::Response::new(Vec::new())) diff --git a/core/runtime/src/fetch/tests/response.rs b/core/runtime/src/fetch/tests/response.rs index 72c9cc809e8..d378cb65495 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -155,3 +155,83 @@ 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(); + }), + ]); +} From 096a0ca95bf0114ee8e56e34b62ac78de32cd1dc Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:42:02 +0530 Subject: [PATCH 2/2] feat: implement the `Response` interface and associated types for the Fetch API. --- core/runtime/src/fetch/response.rs | 42 ++---------------------- core/runtime/src/fetch/tests/response.rs | 13 ++++++-- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/core/runtime/src/fetch/response.rs b/core/runtime/src/fetch/response.rs index e6bb030678a..6a1fd7ae519 100644 --- a/core/runtime/src/fetch/response.rs +++ b/core/runtime/src/fetch/response.rs @@ -320,51 +320,13 @@ impl JsResponse { Ok(Self { url: js_string!(""), r#type: ResponseType::Basic, - status: Some(status_code), + 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()), }) } - /// `Response.json(data, init)` per Fetch spec §7.4. - #[boa(static)] - #[boa(rename = "json")] - fn json_static( - data: JsValue, - init: Option, - context: &mut Context, - ) -> JsResult { - let json_value = data - .to_json(context)? - .ok_or_else(|| js_error!(TypeError: "Cannot serialize undefined to JSON"))?; - let body = serde_json::to_string(&json_value) - .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; - - let status = init.as_ref().and_then(|o| o.status).unwrap_or(200); - let status_code = StatusCode::from_u16(status) - .map_err(|_| js_error!(RangeError: "Invalid status code: {}", status))?; - - let mut headers = init - .as_ref() - .and_then(|o| o.headers.clone()) - .unwrap_or_default(); - // Per spec, set Content-Type only if not already present. - if !headers.has(Convert(String::from("content-type")))? { - headers.append( - Convert(String::from("content-type")), - Convert(String::from("application/json")), - )?; - } - - Ok(Self { - url: js_string!(""), - r#type: ResponseType::Basic, - status: Some(status_code), - headers, - body: Rc::new(body.into_bytes()), - }) - } - /// 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 88f1d4f6dd7..da829ecc7a3 100644 --- a/core/runtime/src/fetch/tests/response.rs +++ b/core/runtime/src/fetch/tests/response.rs @@ -226,6 +226,17 @@ fn response_json_static() { 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([ TestAction::harness(), @@ -248,8 +259,6 @@ fn response_headers_get_combines_duplicate_values_with_comma_space() { "#, ), TestAction::inspect_context(|ctx| { - let p = ctx.global_object().get(js_str!("p"), ctx).unwrap(); - p.as_promise().unwrap().await_blocking(ctx).unwrap(); let response = ctx.global_object().get(js_str!("response"), ctx).unwrap(); response.as_promise().unwrap().await_blocking(ctx).unwrap(); }),