diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index ee30bdf7..f7cdc3b4 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -1236,28 +1236,20 @@ bool Request::clone(JSContext *cx, unsigned argc, JS::Value *vp) { } init_slots(new_request); - RootedValue cloned_headers_val(cx, JS::NullValue()); - RootedObject headers(cx, RequestOrResponse::maybe_headers(self)); - if (headers) { - RootedValue headers_val(cx, ObjectValue(*headers)); - JSObject *cloned_headers = Headers::create(cx, headers_val, Headers::guard(headers)); - if (!cloned_headers) { - return false; - } - cloned_headers_val.set(ObjectValue(*cloned_headers)); - } else if (RequestOrResponse::maybe_handle(self)) { - auto handle = RequestOrResponse::headers_handle_clone(cx, self); - JSObject *cloned_headers = - Headers::create(cx, handle.release(), - RequestOrResponse::is_incoming(self) ? Headers::HeadersGuard::Immutable - : Headers::HeadersGuard::Request); - if (!cloned_headers) { - return false; - } - cloned_headers_val.set(ObjectValue(*cloned_headers)); + auto handle = RequestOrResponse::headers_handle_clone(cx, self); + + auto headers_guard = RequestOrResponse::is_incoming(self) ? Headers::HeadersGuard::Immutable + : Headers::HeadersGuard::Response; + JSObject *cloned_headers = Headers::create(cx, handle.release(), headers_guard); + + if (!cloned_headers) { + return false; } + RootedValue cloned_headers_val(cx, ObjectValue(*cloned_headers)); + SetReservedSlot(new_request, static_cast(Slots::Headers), cloned_headers_val); + Value url_val = GetReservedSlot(self, static_cast(Slots::URL)); SetReservedSlot(new_request, static_cast(Slots::URL), url_val); Value method_val = JS::StringValue(method(self)); @@ -2291,6 +2283,81 @@ bool Response::redirect(JSContext *cx, unsigned argc, Value *vp) { // return true; // } +/// https://fetch.spec.whatwg.org/#dom-response-clone +bool Response::clone(JSContext *cx, unsigned argc, JS::Value *vp) { + // If this is unusable, then throw a TypeError. + METHOD_HEADER(0); + + // To clone a response response, run these steps: + // 1. If response is a filtered response, then return a new identical filtered response whose internal response is a clone of response’s internal response. + RootedObject new_response(cx, create(cx)); + if (!new_response) { + return false; + } + + init_slots(new_response); + + // 2. Let newResponse be a copy of response, except for its body. + auto handle = RequestOrResponse::headers_handle_clone(cx, self); + + auto headers_guard = RequestOrResponse::is_incoming(self) ? Headers::HeadersGuard::Immutable + : Headers::HeadersGuard::Response; + JSObject *cloned_headers = Headers::create(cx, handle.release(), headers_guard); + + if (!cloned_headers) { + return false; + } + + RootedValue cloned_headers_val(cx, ObjectValue(*cloned_headers)); + + SetReservedSlot(new_response, static_cast(Slots::Headers), cloned_headers_val); + + Value status_val = GetReservedSlot(self, static_cast(Slots::Status)); + Value status_message_val = GetReservedSlot(self, static_cast(Slots::StatusMessage)); + Value url_val = GetReservedSlot(self, static_cast(Slots::URL)); + + SetReservedSlot(new_response, static_cast(Slots::Status), status_val); + SetReservedSlot(new_response, static_cast(Slots::StatusMessage), status_message_val); + SetReservedSlot(new_response, static_cast(Slots::URL), url_val); + + // 3. If response’s body is non-null, then set newResponse’s body to the result of cloning response’s body. + RootedObject new_body(cx); + auto has_body = RequestOrResponse::has_body(self); + if (!has_body) { + args.rval().setObject(*new_response); + return true; + } + + // Here we get the current response's body stream and call ReadableStream.prototype.tee to + // get two streams for the same content. + // One of these is then used to replace the current response's body, the other is used as + // the body of the clone. + JS::RootedObject body_stream(cx, RequestOrResponse::body_stream(self)); + if (!body_stream) { + body_stream = RequestOrResponse::create_body_stream(cx, self); + if (!body_stream) { + return false; + } + } + + if (RequestOrResponse::body_unusable(cx, body_stream)) { + return api::throw_error(cx, FetchErrors::BodyStreamUnusable); + } + + RootedObject self_body(cx); + if (!ReadableStreamTee(cx, body_stream, &self_body, &new_body)) { + return false; + } + + SetReservedSlot(self, static_cast(Slots::BodyStream), ObjectValue(*self_body)); + SetReservedSlot(new_response, static_cast(Slots::BodyStream), ObjectValue(*new_body)); + SetReservedSlot(new_response, static_cast(Slots::HasBody), JS::BooleanValue(true)); + + // 4. Return newResponse. + args.rval().setObject(*new_response); + return true; +} + const JSFunctionSpec Response::static_methods[] = { JS_FN("redirect", redirect, 1, JSPROP_ENUMERATE), // JS_FN("json", json, 1, JSPROP_ENUMERATE), @@ -2306,6 +2373,7 @@ const JSFunctionSpec Response::methods[] = { JSPROP_ENUMERATE), JS_FN("json", bodyAll, 0, JSPROP_ENUMERATE), JS_FN("text", bodyAll, 0, JSPROP_ENUMERATE), + JS_FN("clone", clone, 0, JSPROP_ENUMERATE), JS_FS_END, }; diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index b963ca25..ce4944e0 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -180,6 +180,8 @@ class Response final : public BuiltinImpl { static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp); static bool json(JSContext *cx, unsigned argc, JS::Value *vp); + static bool clone(JSContext *cx, unsigned argc, JS::Value *vp); + public: static constexpr const char *class_name = "Response"; @@ -189,6 +191,7 @@ class Response final : public BuiltinImpl { HasBody = static_cast(RequestOrResponse::Slots::HasBody), BodyUsed = static_cast(RequestOrResponse::Slots::BodyUsed), Headers = static_cast(RequestOrResponse::Slots::Headers), + URL = static_cast(RequestOrResponse::Slots::URL), Status = static_cast(RequestOrResponse::Slots::Count), StatusMessage, Redirected, diff --git a/tests/integration/fetch/fetch.js b/tests/integration/fetch/fetch.js index 997976cf..4728144d 100644 --- a/tests/integration/fetch/fetch.js +++ b/tests/integration/fetch/fetch.js @@ -21,7 +21,10 @@ export const handler = serveTest(async (t) => { body: 'te', method: 'post' }); + + request.headers.set("foo", "bar") const newRequest = request.clone(); + strictEqual(newRequest instanceof Request, true, 'newRequest instanceof Request'); strictEqual(newRequest.method, request.method, 'newRequest.method'); strictEqual(newRequest.url, request.url, 'newRequest.url'); @@ -29,6 +32,11 @@ export const handler = serveTest(async (t) => { strictEqual(request.bodyUsed, false, 'request.bodyUsed'); strictEqual(newRequest.bodyUsed, false, 'newRequest.bodyUsed'); strictEqual(newRequest.body instanceof ReadableStream, true, 'newRequest.body instanceof ReadableStream'); + + strictEqual(newRequest.headers.get("foo"), "bar", 'newRequest.status pre-modification'); + request.headers.set("foo", "bao") + strictEqual(newRequest.headers.get("foo"), "bar", 'newRequest.status post-modification'); + strictEqual(request.headers.get("foo"), "bao", 'request.status post-modification'); } { @@ -53,4 +61,55 @@ export const handler = serveTest(async (t) => { await request.text(); throws(() => request.clone()); }); + + t.test('response-clone-bad-calls', () => { + throws(() => new Response.prototype.clone(), TypeError); + throws(() => new Response.prototype.clone.call(undefined), TypeError); + }); + + await t.test('response-clone-valid', async () => { + { + const response = new Response('test body', { + headers: { + hello: 'world' + }, + status: 200, + statusText: 'Success' + }); + response.headers.set("foo", "bar") + const newResponse = response.clone(); + + strictEqual(newResponse instanceof Response, true, 'newResponse instanceof Request'); + strictEqual(response.bodyUsed, false, 'response.bodyUsed'); + strictEqual(newResponse.bodyUsed, false, 'newResponse.bodyUsed'); + deepStrictEqual([...newResponse.headers], [...response.headers], 'newResponse.headers'); + strictEqual(newResponse.status, 200, 'newResponse.status'); + strictEqual(newResponse.statusText, 'Success', 'newResponse.statusText'); + strictEqual(newResponse.body instanceof ReadableStream, true, 'newResponse.body instanceof ReadableStream'); + + strictEqual(newResponse.headers.get("foo"), "bar", 'newResponse.status pre-modification'); + response.headers.set("foo", "bao") + strictEqual(newResponse.headers.get("foo"), "bar", 'newResponse.status post-modification'); + strictEqual(response.headers.get("foo"), "bao", 'response.status post-modification'); + } + + { + const response = new Response(null, { + status: 404, + statusText: "Not found", + }); + const newResponse = response.clone(); + strictEqual(newResponse.bodyUsed, false, 'newResponse.bodyUsed'); + strictEqual(newResponse.body, null, 'newResponse.body'); + } + }); + + await t.test('response-clone-invalid', async () => { + const response = new Response('test body', { + status: 200, + statusText: "Success" + }); + await response.text(); + throws(() => response.clone()); + }); });