Skip to content

Commit 3f3a671

Browse files
Jake ChampionJakeChampion
Jake Champion
authored andcommitted
feat: implement Request.prototype.clone
This commit adds a native implementation for Request.prototype.clone - The clone() method creates a copy of the current Request object.
1 parent aac60a3 commit 3f3a671

File tree

6 files changed

+268
-4
lines changed

6 files changed

+268
-4
lines changed

.github/workflows/main.yml

+2
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ jobs:
189189
- random
190190
- regex
191191
- request-cache-key
192+
- request-clone
192193
- request-downstream
193194
- request-limits
194195
- request-upstream
@@ -310,6 +311,7 @@ jobs:
310311
- 'hello-world'
311312
- 'multiple-set-cookie'
312313
- 'object-store'
314+
- 'request-clone'
313315
- 'request-limits'
314316
- 'request-upstream'
315317
- 'response'

c-dependencies/js-compute-runtime/js-compute-builtins.cpp

+129-3
Original file line numberDiff line numberDiff line change
@@ -1614,14 +1614,136 @@ bool setCacheKey(JSContext *cx, unsigned argc, Value *vp) {
16141614
args.rval().setUndefined();
16151615
return true;
16161616
}
1617+
JSString *GET_atom;
1618+
JSObject *create_instance(JSContext *cx);
1619+
JSObject *create(JSContext *cx, HandleObject requestInstance, HandleValue input,
1620+
HandleValue init_val);
1621+
1622+
bool clone(JSContext *cx, unsigned argc, Value *vp) {
1623+
METHOD_HEADER(0)
1624+
1625+
if (RequestOrResponse::body_used(self)) {
1626+
JS_ReportErrorLatin1(cx, "Request.prototype.clone: the request's body isn't usable.");
1627+
return false;
1628+
}
1629+
1630+
// Here we get the current requests body stream and call ReadableStream.prototype.tee to return
1631+
// two versions of the stream. Once we get the two streams, we create a new request handle and
1632+
// attach one of the streams to the new handle and the other stream is attached to the request
1633+
// handle that `clone()` was called upon.
1634+
RootedObject body_stream(cx, ::RequestOrResponse::body_stream(self));
1635+
if (!body_stream) {
1636+
body_stream = ::RequestOrResponse::create_body_stream(cx, self);
1637+
if (!body_stream) {
1638+
return false;
1639+
}
1640+
}
1641+
RootedValue tee_val(cx);
1642+
if (!JS_GetProperty(cx, body_stream, "tee", &tee_val)) {
1643+
return false;
1644+
}
1645+
JS::Rooted<JSFunction *> tee(cx, JS_GetObjectFunction(&tee_val.toObject()));
1646+
if (!tee) {
1647+
return false;
1648+
}
1649+
JS::RootedVector<JS::Value> argv(cx);
1650+
RootedValue rval(cx);
1651+
if (!JS::Call(cx, body_stream, tee, argv, &rval)) {
1652+
return false;
1653+
}
1654+
RootedObject rval_array(cx, &rval.toObject());
1655+
RootedValue body1_val(cx);
1656+
if (!JS_GetProperty(cx, rval_array, "0", &body1_val)) {
1657+
return false;
1658+
}
1659+
RootedValue body2_val(cx);
1660+
if (!JS_GetProperty(cx, rval_array, "1", &body2_val)) {
1661+
return false;
1662+
}
1663+
1664+
fastly_error_t err;
1665+
fastly_request_handle_t request_handle = INVALID_HANDLE;
1666+
if (!xqd_fastly_http_req_new(&request_handle, &err)) {
1667+
HANDLE_ERROR(cx, err);
1668+
return false;
1669+
}
1670+
1671+
fastly_body_handle_t body_handle = INVALID_HANDLE;
1672+
if (!xqd_fastly_http_body_new(&body_handle, &err)) {
1673+
HANDLE_ERROR(cx, err);
1674+
return false;
1675+
}
1676+
1677+
if (!JS::IsReadableStream(&body1_val.toObject())) {
1678+
return false;
1679+
}
1680+
body_stream.set(&body1_val.toObject());
1681+
if (RequestOrResponse::body_unusable(cx, body_stream)) {
1682+
JS_ReportErrorLatin1(cx, "Can't use a ReadableStream that's locked or has ever been "
1683+
"read from or canceled as a Request body.");
1684+
return false;
1685+
}
1686+
1687+
RootedObject requestInstance(cx, create_instance(cx));
1688+
JS::SetReservedSlot(requestInstance, Slots::Request, JS::Int32Value(request_handle));
1689+
JS::SetReservedSlot(requestInstance, Slots::Body, JS::Int32Value(body_handle));
1690+
JS::SetReservedSlot(requestInstance, Slots::BodyStream, body1_val);
1691+
JS::SetReservedSlot(requestInstance, Slots::BodyUsed, JS::FalseValue());
1692+
JS::SetReservedSlot(requestInstance, Slots::HasBody, JS::BooleanValue(true));
1693+
JS::SetReservedSlot(requestInstance, Slots::URL, JS::GetReservedSlot(self, Slots::URL));
1694+
JS::SetReservedSlot(requestInstance, Slots::IsDownstream,
1695+
JS::GetReservedSlot(self, Slots::IsDownstream));
1696+
1697+
JS::SetReservedSlot(self, Slots::BodyStream, body2_val);
1698+
JS::SetReservedSlot(self, Slots::BodyUsed, JS::FalseValue());
1699+
JS::SetReservedSlot(self, Slots::HasBody, JS::BooleanValue(true));
1700+
1701+
RootedObject headers(cx);
1702+
RootedObject headers_obj(cx, RequestOrResponse::headers<Headers::Mode::ProxyToRequest>(cx, self));
1703+
if (!headers_obj) {
1704+
return false;
1705+
}
1706+
RootedObject headersInstance(
1707+
cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj));
1708+
if (!headersInstance)
1709+
return false;
1710+
1711+
headers = Headers::create(cx, headersInstance, Headers::Mode::ProxyToRequest, requestInstance,
1712+
headers_obj);
1713+
1714+
if (!headers) {
1715+
return false;
1716+
}
1717+
1718+
JS::SetReservedSlot(requestInstance, Slots::Headers, JS::ObjectValue(*headers));
1719+
1720+
JSString *method = Request::method(cx, self);
1721+
if (!method) {
1722+
return false;
1723+
}
1724+
1725+
JS::SetReservedSlot(requestInstance, Slots::Method, JS::StringValue(method));
1726+
RootedValue cache_override(cx, JS::GetReservedSlot(self, Slots::CacheOverride));
1727+
if (!cache_override.isNullOrUndefined()) {
1728+
if (!set_cache_override(cx, requestInstance, cache_override)) {
1729+
return false;
1730+
}
1731+
} else {
1732+
JS::SetReservedSlot(requestInstance, Slots::CacheOverride, cache_override);
1733+
}
1734+
1735+
args.rval().setObject(*requestInstance);
1736+
return true;
1737+
}
16171738

16181739
const JSFunctionSpec methods[] = {
16191740
JS_FN("arrayBuffer", bodyAll<RequestOrResponse::BodyReadResult::ArrayBuffer>, 0,
16201741
JSPROP_ENUMERATE),
16211742
JS_FN("json", bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
16221743
JS_FN("text", bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
16231744
JS_FN("setCacheOverride", setCacheOverride, 3, JSPROP_ENUMERATE),
1624-
JS_FN("setCacheKey", setCacheKey, 3, JSPROP_ENUMERATE),
1745+
JS_FN("setCacheKey", setCacheKey, 0, JSPROP_ENUMERATE),
1746+
JS_FN("clone", clone, 0, JSPROP_ENUMERATE),
16251747
JS_FS_END};
16261748

16271749
const JSPropertySpec properties[] = {JS_PSG("method", method_get, JSPROP_ENUMERATE),
@@ -1637,8 +1759,6 @@ bool constructor(JSContext *cx, unsigned argc, Value *vp);
16371759

16381760
CLASS_BOILERPLATE_CUSTOM_INIT(Request)
16391761

1640-
JSString *GET_atom;
1641-
16421762
bool init_class(JSContext *cx, HandleObject global) {
16431763
if (!init_class_impl(cx, global)) {
16441764
return false;
@@ -2132,6 +2252,12 @@ JSObject *create(JSContext *cx, HandleObject requestInstance, HandleValue input,
21322252
return request;
21332253
}
21342254

2255+
JSObject *create_instance(JSContext *cx) {
2256+
RootedObject requestInstance(
2257+
cx, JS_NewObjectWithGivenProto(cx, &Request::class_, Request::proto_obj));
2258+
return requestInstance;
2259+
}
2260+
21352261
bool constructor(JSContext *cx, unsigned argc, Value *vp) {
21362262
REQUEST_HANDLER_ONLY("The Request builtin");
21372263
CTOR_HEADER("Request", 1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* eslint-env serviceworker */
2+
import { env } from 'fastly:env';
3+
import { pass, fail, assert, assertThrows } from "../../../assertions.js";
4+
5+
addEventListener("fetch", event => {
6+
event.respondWith(app(event))
7+
})
8+
/**
9+
* @param {FetchEvent} event
10+
* @returns {Response}
11+
*/
12+
function app(event) {
13+
try {
14+
const path = (new URL(event.request.url)).pathname;
15+
console.log(`path: ${path}`)
16+
console.log(`FASTLY_SERVICE_VERSION: ${env('FASTLY_SERVICE_VERSION')}`)
17+
if (routes.has(path)) {
18+
const routeHandler = routes.get(path);
19+
return routeHandler()
20+
}
21+
return fail(`${path} endpoint does not exist`)
22+
} catch (error) {
23+
return fail(`The routeHandler threw an error: ${error.message}` + '\n' + error.stack)
24+
}
25+
}
26+
27+
const routes = new Map();
28+
routes.set('/', () => {
29+
routes.delete('/');
30+
let test_routes = Array.from(routes.keys())
31+
return new Response(JSON.stringify(test_routes), { 'headers': { 'content-type': 'application/json' } });
32+
});
33+
routes.set("/request/clone/called-as-constructor", () => {
34+
let error = assertThrows(() => {
35+
new Request.prototype.clone()
36+
}, TypeError, `Request.prototype.clone is not a constructor`)
37+
if (error) { return error }
38+
return pass()
39+
});
40+
routes.set("/request/clone/called-unbound", () => {
41+
let error = assertThrows(() => {
42+
Request.prototype.clone.call(undefined)
43+
}, TypeError, "Method clone called on receiver that's not an instance of Request")
44+
if (error) { return error }
45+
return pass()
46+
});
47+
routes.set("/request/clone/valid", async () => {
48+
const request = new Request('https://www.fastly.com', {
49+
headers: {
50+
hello: 'world'
51+
},
52+
body: 'te',
53+
method: 'post'
54+
})
55+
const newRequest = request.clone();
56+
let error = assert(newRequest instanceof Request, true, 'newRequest instanceof Request')
57+
if (error) { return error }
58+
error = assert(newRequest.method, request.method, 'newRequest.method === request.method')
59+
if (error) { return error }
60+
error = assert(newRequest.url, request.url, 'newRequest.url === request.url')
61+
if (error) { return error }
62+
error = assert(newRequest.headers, request.headers, 'newRequest.headers === request.headers')
63+
if (error) { return error }
64+
error = assert(request.bodyUsed, false, 'request.bodyUsed === false')
65+
if (error) { return error }
66+
error = assert(newRequest.bodyUsed, false, 'newRequest.bodyUsed === false')
67+
if (error) { return error }
68+
return pass()
69+
});
70+
routes.set("/request/clone/invalid", async () => {
71+
const request = new Request('https://www.fastly.com', {
72+
headers: {
73+
hello: 'world'
74+
},
75+
body: 'te',
76+
method: 'post'
77+
})
78+
await request.text()
79+
let error = assertThrows(() => request.clone())
80+
if (error) { return error }
81+
return pass()
82+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This file describes a Fastly Compute@Edge package. To learn more visit:
2+
# https://developer.fastly.com/reference/fastly-toml/
3+
4+
authors = ["[email protected]"]
5+
description = ""
6+
language = "other"
7+
manifest_version = 2
8+
name = "request-clone"
9+
service_id = ""
10+
11+
[scripts]
12+
build = "node ../../../../js-compute-runtime-cli.js"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"GET /request/clone/called-as-constructor": {
3+
"environments": ["viceroy", "c@e"],
4+
"downstream_request": {
5+
"method": "GET",
6+
"pathname": "/request/clone/called-as-constructor"
7+
},
8+
"downstream_response": {
9+
"status": 200
10+
}
11+
},
12+
"GET /request/clone/called-unbound": {
13+
"environments": ["viceroy", "c@e"],
14+
"downstream_request": {
15+
"method": "GET",
16+
"pathname": "/request/clone/called-unbound"
17+
},
18+
"downstream_response": {
19+
"status": 200
20+
}
21+
},
22+
"GET /request/clone/valid": {
23+
"environments": ["viceroy", "c@e"],
24+
"downstream_request": {
25+
"method": "GET",
26+
"pathname": "/request/clone/valid"
27+
},
28+
"downstream_response": {
29+
"status": 200
30+
}
31+
},
32+
"GET /request/clone/invalid": {
33+
"environments": ["viceroy", "c@e"],
34+
"downstream_request": {
35+
"method": "GET",
36+
"pathname": "/request/clone/invalid"
37+
},
38+
"downstream_response": {
39+
"status": 200
40+
}
41+
}
42+
}

tests/wpt-harness/expectations/fetch/api/request/request-structure.any.js.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"Request has clone method": {
3-
"status": "FAIL"
3+
"status": "PASS"
44
},
55
"Request has arrayBuffer method": {
66
"status": "PASS"

0 commit comments

Comments
 (0)