Skip to content

Commit 7f98c6c

Browse files
authored
Merge branch 'main' into feat/res2
2 parents 86e46c5 + 50f0103 commit 7f98c6c

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

core/runtime/src/fetch/headers.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ impl JsHeaders {
7474
pub fn as_header_map(&self) -> Rc<RefCell<HttpHeaderMap>> {
7575
self.headers.clone()
7676
}
77+
78+
pub(crate) fn deep_clone(&self) -> Self {
79+
Self {
80+
headers: Rc::new(RefCell::new((*self.headers.borrow()).clone())),
81+
}
82+
}
7783
}
7884

7985
#[boa_class(rename = "Headers")]

core/runtime/src/fetch/request.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,11 @@ impl JsRequest {
178178
};
179179
JsRequest::create_from_js(input, options)
180180
}
181+
182+
#[boa(rename = "clone")]
183+
fn clone_request(&self) -> Self {
184+
Self {
185+
inner: self.inner.clone(),
186+
}
187+
}
181188
}

core/runtime/src/fetch/response.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use boa_engine::{
1313
js_string,
1414
};
1515
use boa_gc::{Finalize, Trace};
16+
use http::{HeaderName, HeaderValue, StatusCode};
1617
use std::rc::Rc;
1718

1819
/// The type read-only property of the Response interface contains the type of the
@@ -295,6 +296,37 @@ impl JsResponse {
295296
Self::error()
296297
}
297298

299+
/// `Response.redirect(url, status)` per Fetch spec §7.4.
300+
#[boa(static)]
301+
fn redirect(url: JsValue, status: Option<u16>, context: &mut Context) -> JsResult<Self> {
302+
let status = status.unwrap_or(302);
303+
if !matches!(status, 301 | 302 | 303 | 307 | 308) {
304+
return Err(js_error!(RangeError: "Invalid redirect status: {}", status));
305+
}
306+
let url_str = url.to_string(context)?.to_std_string_escaped();
307+
http::Uri::try_from(url_str.as_str())
308+
.map_err(|_| js_error!(TypeError: "Invalid URL: {}", url_str))?;
309+
310+
let status_code = StatusCode::from_u16(status)
311+
.map_err(|_| js_error!(RangeError: "Invalid status code: {}", status))?;
312+
313+
let mut headers = http::header::HeaderMap::new();
314+
headers.insert(
315+
HeaderName::from_static("location"),
316+
HeaderValue::try_from(url_str)
317+
.map_err(|_| js_error!(TypeError: "Invalid URL for header value"))?,
318+
);
319+
320+
Ok(Self {
321+
url: js_string!(""),
322+
r#type: ResponseType::Basic,
323+
status: status_code.as_u16(),
324+
status_text: JsString::from(status_code.canonical_reason().unwrap_or("")),
325+
headers: JsHeaders::from_http(headers),
326+
body: Rc::new(Vec::new()),
327+
})
328+
}
329+
298330
/// Creates a `Response` with a JSON-serialized body and `Content-Type: application/json`.
299331
///
300332
/// See <https://fetch.spec.whatwg.org/#dom-response-json>
@@ -414,6 +446,18 @@ impl JsResponse {
414446
false
415447
}
416448

449+
#[boa(rename = "clone")]
450+
fn clone_response(&self) -> Self {
451+
Self {
452+
url: self.url.clone(),
453+
r#type: self.r#type,
454+
status: self.status,
455+
status_text: self.status_text.clone(),
456+
headers: self.headers.deep_clone(),
457+
body: Rc::new((*self.body).clone()),
458+
}
459+
}
460+
417461
fn bytes(&self, context: &mut Context) -> JsPromise {
418462
let body = self.body.clone();
419463
JsPromise::from_async_fn(

core/runtime/src/fetch/tests/request.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,66 @@ fn request_clone_no_body_preserved() {
152152
}),
153153
]);
154154
}
155+
156+
#[test]
157+
fn request_clone_method_preserves_body() {
158+
run_test_actions([
159+
TestAction::inspect_context(|ctx| {
160+
let fetcher = TestFetcher::default();
161+
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
162+
}),
163+
TestAction::run(
164+
r#"
165+
const original = new Request("http://unit.test", {
166+
method: "POST",
167+
body: "payload",
168+
});
169+
globalThis.cloned = original.clone();
170+
"#,
171+
),
172+
TestAction::inspect_context(|ctx| {
173+
let cloned = ctx.global_object().get(js_str!("cloned"), ctx).unwrap();
174+
let cloned_obj = cloned.as_object().unwrap();
175+
let cloned_req = cloned_obj.downcast_ref::<JsRequest>().unwrap();
176+
assert_eq!(cloned_req.inner().body().as_slice(), b"payload");
177+
}),
178+
]);
179+
}
180+
181+
#[test]
182+
fn request_clone_method_is_independent() {
183+
run_test_actions([
184+
TestAction::inspect_context(|ctx| {
185+
let fetcher = TestFetcher::default();
186+
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
187+
}),
188+
TestAction::run(
189+
r#"
190+
const original = new Request("http://unit.test", {
191+
method: "POST",
192+
body: "original-body",
193+
});
194+
globalThis.original = original;
195+
globalThis.cloned = original.clone();
196+
"#,
197+
),
198+
TestAction::inspect_context(|ctx| {
199+
let original = ctx.global_object().get(js_str!("original"), ctx).unwrap();
200+
let original_obj = original.as_object().unwrap();
201+
let original_req = original_obj.downcast_ref::<JsRequest>().unwrap();
202+
203+
let cloned = ctx.global_object().get(js_str!("cloned"), ctx).unwrap();
204+
let cloned_obj = cloned.as_object().unwrap();
205+
let cloned_req = cloned_obj.downcast_ref::<JsRequest>().unwrap();
206+
207+
assert_eq!(original_req.inner().body().as_slice(), b"original-body");
208+
assert_eq!(cloned_req.inner().body().as_slice(), b"original-body");
209+
210+
// Verify they are distinct objects (different pointers).
211+
assert!(!std::ptr::eq(
212+
original_req.inner().body().as_ptr(),
213+
cloned_req.inner().body().as_ptr()
214+
));
215+
}),
216+
]);
217+
}

core/runtime/src/fetch/tests/response.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,86 @@ fn response_getter() {
156156
]);
157157
}
158158

159+
#[test]
160+
fn response_redirect_default_status() {
161+
run_test_actions([
162+
TestAction::harness(),
163+
TestAction::inspect_context(|ctx| register(&[], ctx)),
164+
TestAction::run(
165+
r#"
166+
const response = Response.redirect("http://example.com/");
167+
assertEq(response.status, 302);
168+
assertEq(response.headers.get("location"), "http://example.com/");
169+
"#,
170+
),
171+
]);
172+
}
173+
174+
#[test]
175+
fn response_redirect_custom_status_and_coercion() {
176+
run_test_actions([
177+
TestAction::harness(),
178+
TestAction::inspect_context(|ctx| register(&[], ctx)),
179+
TestAction::run(
180+
r#"
181+
const response = Response.redirect("http://example.com/", 301);
182+
assertEq(response.status, 301);
183+
184+
// Tests Web IDL coercion of the URL parameter
185+
const response2 = Response.redirect(12345);
186+
assertEq(response2.headers.get("location"), "12345");
187+
"#,
188+
),
189+
]);
190+
}
191+
192+
#[test]
193+
fn response_redirect_invalid_status() {
194+
run_test_actions([
195+
TestAction::harness(),
196+
TestAction::inspect_context(|ctx| register(&[], ctx)),
197+
TestAction::run(
198+
r#"
199+
let threw = false;
200+
try {
201+
Response.redirect("http://example.com/", 200);
202+
} catch (e) {
203+
threw = true;
204+
if (!(e instanceof RangeError)) {
205+
throw new Error("Expected RangeError, got " + e.name);
206+
}
207+
}
208+
if (!threw) {
209+
throw new Error("Expected RangeError, but no error was thrown");
210+
}
211+
"#,
212+
),
213+
]);
214+
}
215+
216+
#[test]
217+
fn response_json_static() {
218+
run_test_actions([
219+
TestAction::harness(),
220+
TestAction::inspect_context(|ctx| register(&[], ctx)),
221+
TestAction::run(
222+
r#"
223+
globalThis.p = (async () => {
224+
const response = Response.json({ hello: "world" });
225+
assertEq(response.status, 200);
226+
assertEq(response.headers.get("content-type"), "application/json");
227+
const body = await response.json();
228+
assertEq(body.hello, "world");
229+
})();
230+
"#,
231+
),
232+
TestAction::inspect_context(|ctx| {
233+
let p = ctx.global_object().get(js_str!("p"), ctx).unwrap();
234+
p.as_promise().unwrap().await_blocking(ctx).unwrap();
235+
}),
236+
]);
237+
}
238+
159239
#[test]
160240
fn response_headers_get_combines_duplicate_values_with_comma_space() {
161241
run_test_actions([
@@ -184,3 +264,93 @@ fn response_headers_get_combines_duplicate_values_with_comma_space() {
184264
}),
185265
]);
186266
}
267+
268+
#[test]
269+
fn response_clone_read_both() {
270+
run_test_actions([
271+
TestAction::harness(),
272+
TestAction::inspect_context(|ctx| {
273+
register(
274+
&[("http://unit.test", Response::new(b"Hello World".to_vec()))],
275+
ctx,
276+
);
277+
}),
278+
TestAction::run(
279+
r#"
280+
globalThis.response = (async () => {
281+
const response = await fetch(new Request("http://unit.test"));
282+
const cloned = response.clone();
283+
const t1 = await response.text();
284+
const t2 = await cloned.text();
285+
assertEq(t1, "Hello World");
286+
assertEq(t2, "Hello World");
287+
})();
288+
"#,
289+
),
290+
TestAction::inspect_context(|ctx| {
291+
let response = ctx.global_object().get(js_str!("response"), ctx).unwrap();
292+
response.as_promise().unwrap().await_blocking(ctx).unwrap();
293+
}),
294+
]);
295+
}
296+
297+
#[test]
298+
fn response_clone_header_independence() {
299+
run_test_actions([
300+
TestAction::harness(),
301+
TestAction::inspect_context(|ctx| {
302+
let mut resp = Response::new(b"body".to_vec());
303+
resp.headers_mut()
304+
.append("x-original", "value".parse().unwrap());
305+
register(&[("http://unit.test", resp)], ctx);
306+
}),
307+
TestAction::run(
308+
r#"
309+
globalThis.response = (async () => {
310+
const response = await fetch(new Request("http://unit.test"));
311+
const cloned = response.clone();
312+
313+
cloned.headers.set("x-original", "mutated");
314+
cloned.headers.set("x-new", "added");
315+
316+
assertEq(response.headers.get("x-original"), "value");
317+
assertEq(response.headers.get("x-new"), null);
318+
assertEq(cloned.headers.get("x-original"), "mutated");
319+
assertEq(cloned.headers.get("x-new"), "added");
320+
})();
321+
"#,
322+
),
323+
TestAction::inspect_context(|ctx| {
324+
let response = ctx.global_object().get(js_str!("response"), ctx).unwrap();
325+
response.as_promise().unwrap().await_blocking(ctx).unwrap();
326+
}),
327+
]);
328+
}
329+
330+
#[test]
331+
fn response_clone_preserves_status() {
332+
run_test_actions([
333+
TestAction::harness(),
334+
TestAction::inspect_context(|ctx| {
335+
register(&[("http://unit.test", Response::new(b"ok".to_vec()))], ctx);
336+
}),
337+
TestAction::run(
338+
r#"
339+
globalThis.response = (async () => {
340+
const response = await fetch(new Request("http://unit.test"));
341+
const cloned = response.clone();
342+
343+
assertEq(cloned.status, response.status);
344+
assertEq(cloned.statusText, response.statusText);
345+
assertEq(cloned.type, response.type);
346+
assertEq(cloned.url, response.url);
347+
assertEq(cloned.ok, response.ok);
348+
})();
349+
"#,
350+
),
351+
TestAction::inspect_context(|ctx| {
352+
let response = ctx.global_object().get(js_str!("response"), ctx).unwrap();
353+
response.as_promise().unwrap().await_blocking(ctx).unwrap();
354+
}),
355+
]);
356+
}

0 commit comments

Comments
 (0)