Skip to content

Commit 4f5ce51

Browse files
vazexqiclaude
andcommitted
feat: implement Response.prototype.clone()
Add the missing clone() method to the Response class, following the same pattern as Request.clone(). The implementation copies all response properties (status, statusText, headers, url, type, redirected) and uses ReadableStreamTee to fork the body stream when present. This enables response.clone() calls in the Zuplo runtime pipeline (e.g., AkamaiApiSecurityPlugin) without needing a JS polyfill. Ref: upstream issue bytecodealliance#125, draft PR bytecodealliance#178. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df1eecb commit 4f5ce51

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

builtins/web/fetch/request-response.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,101 @@ bool Response::json(JSContext *cx, unsigned argc, JS::Value *vp) {
25262526
}
25272527

25282528

2529+
/// https://fetch.spec.whatwg.org/#dom-response-clone
2530+
bool Response::clone(JSContext *cx, unsigned argc, JS::Value *vp) {
2531+
METHOD_HEADER(0);
2532+
2533+
// Step 1: If this is unusable (i.e. disturbed or locked), throw a TypeError.
2534+
// We check body usability below when the body exists.
2535+
2536+
// Step 2: Let clonedResponse be a copy of this's response, except for its body.
2537+
RootedObject new_response(cx, create(cx));
2538+
if (!new_response) {
2539+
return false;
2540+
}
2541+
2542+
// Copy headers.
2543+
RootedValue cloned_headers_val(cx, JS::NullValue());
2544+
RootedObject headers(cx, RequestOrResponse::maybe_headers(self));
2545+
if (headers) {
2546+
RootedValue headers_val(cx, ObjectValue(*headers));
2547+
JSObject *cloned_headers = Headers::create(cx, headers_val, Headers::guard(headers));
2548+
if (!cloned_headers) {
2549+
return false;
2550+
}
2551+
cloned_headers_val.set(ObjectValue(*cloned_headers));
2552+
} else if (RequestOrResponse::maybe_handle(self)) {
2553+
auto handle = RequestOrResponse::headers_handle_clone(cx, self);
2554+
JSObject *cloned_headers =
2555+
Headers::create(cx, handle.release(),
2556+
RequestOrResponse::is_incoming(self) ? Headers::HeadersGuard::Immutable
2557+
: Headers::HeadersGuard::Response);
2558+
if (!cloned_headers) {
2559+
return false;
2560+
}
2561+
cloned_headers_val.set(ObjectValue(*cloned_headers));
2562+
}
2563+
2564+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::Headers), cloned_headers_val);
2565+
2566+
// Copy URL.
2567+
Value url_val = GetReservedSlot(self, static_cast<uint32_t>(Slots::URL));
2568+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::URL), url_val);
2569+
2570+
// Copy status.
2571+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::Status),
2572+
GetReservedSlot(self, static_cast<uint32_t>(Slots::Status)));
2573+
2574+
// Copy status message.
2575+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::StatusMessage),
2576+
GetReservedSlot(self, static_cast<uint32_t>(Slots::StatusMessage)));
2577+
2578+
// Copy type.
2579+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::Type),
2580+
GetReservedSlot(self, static_cast<uint32_t>(Slots::Type)));
2581+
2582+
// Copy redirected.
2583+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::Redirected),
2584+
GetReservedSlot(self, static_cast<uint32_t>(Slots::Redirected)));
2585+
2586+
// Copy aborted.
2587+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::Aborted),
2588+
GetReservedSlot(self, static_cast<uint32_t>(Slots::Aborted)));
2589+
2590+
// Step 3: If this's response's body is non-null, clone the body.
2591+
auto has_body = RequestOrResponse::has_body(self);
2592+
if (!has_body) {
2593+
args.rval().setObject(*new_response);
2594+
return true;
2595+
}
2596+
2597+
// Get (or create) the body stream, then tee it.
2598+
JS::RootedObject body_stream(cx, RequestOrResponse::body_stream(self));
2599+
if (!body_stream) {
2600+
body_stream = RequestOrResponse::create_body_stream(cx, self);
2601+
if (!body_stream) {
2602+
return false;
2603+
}
2604+
}
2605+
2606+
if (RequestOrResponse::body_unusable(cx, body_stream)) {
2607+
return api::throw_error(cx, FetchErrors::BodyStreamUnusable);
2608+
}
2609+
2610+
RootedObject self_body(cx);
2611+
RootedObject new_body(cx);
2612+
if (!ReadableStreamTee(cx, body_stream, &self_body, &new_body)) {
2613+
return false;
2614+
}
2615+
2616+
SetReservedSlot(self, static_cast<uint32_t>(Slots::BodyStream), ObjectValue(*self_body));
2617+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::BodyStream), ObjectValue(*new_body));
2618+
SetReservedSlot(new_response, static_cast<uint32_t>(Slots::HasBody), JS::BooleanValue(true));
2619+
2620+
args.rval().setObject(*new_response);
2621+
return true;
2622+
}
2623+
25292624
const JSFunctionSpec Response::static_methods[] = {
25302625
JS_FN("redirect", redirect, 1, JSPROP_ENUMERATE),
25312626
JS_FN("json", json, 1, JSPROP_ENUMERATE),
@@ -2543,6 +2638,7 @@ const JSFunctionSpec Response::methods[] = {
25432638
JS_FN("formData", bodyAll<RequestOrResponse::BodyReadResult::FormData>, 0, JSPROP_ENUMERATE),
25442639
JS_FN("json", bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
25452640
JS_FN("text", bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
2641+
JS_FN("clone", Response::clone, 0, JSPROP_ENUMERATE),
25462642
JS_FS_END,
25472643
};
25482644

builtins/web/fetch/request-response.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ class Response final : public BuiltinImpl<Response> {
188188
static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
189189
static bool json(JSContext *cx, unsigned argc, JS::Value *vp);
190190

191+
static bool clone(JSContext *cx, unsigned argc, JS::Value *vp);
192+
191193
public:
192194
static constexpr const char *class_name = "Response";
193195

0 commit comments

Comments
 (0)