Skip to content

Commit 1a1dc3d

Browse files
tschneidereitGuy Bedford
andauthored
Implement Request.clone (#92)
* Cleanup: make `Request::method` infallible It already is infallible internally, so might as well reflect that in the signature and clean up the callsites. * Bugfix: always reify input headers in the `Request` initializer I stumbled upon this while implementing `Request.clone`, and while this change doesn't cause any additional WPT tests to pass, I'm certain that it's required: otherwise, `new Request(incomingRequest)` will not apply `incomingRequest`'s header entries to the new request if `.headers` has never been read on `incomingRequest`. * Implement `Request.clone` Fixes #84 * add simple clone test * fix headers clone --------- Co-authored-by: Guy Bedford <gbedford@fastly.com>
1 parent ea1ad37 commit 1a1dc3d

10 files changed

Lines changed: 152 additions & 149 deletions

File tree

builtins/web/fetch/fetch-api.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) {
3535
return ReturnPromiseRejectedWithPendingError(cx, args);
3636
}
3737

38-
RootedString method_str(cx, Request::method(cx, request_obj));
39-
if (!method_str) {
40-
return ReturnPromiseRejectedWithPendingError(cx, args);
41-
}
42-
38+
RootedString method_str(cx, Request::method(request_obj));
4339
host_api::HostString method = core::encode(cx, method_str);
4440
if (!method.ptr) {
4541
return ReturnPromiseRejectedWithPendingError(cx, args);

builtins/web/fetch/fetch-errors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ DEF_ERR(InvalidInitArg, JSEXN_TYPEERR, "{0}: |init| parameter can't be converted
1212
DEF_ERR(NonBodyRequestWithBody, JSEXN_TYPEERR, "Request constructor: HEAD or GET Request cannot have a body", 0)
1313
DEF_ERR(NonBodyResponseWithBody, JSEXN_TYPEERR, "Response constructor: response with status {0} cannot have a body", 1)
1414
DEF_ERR(BodyStreamUnusable, JSEXN_TYPEERR, "Can't use a ReadableStream that's locked or has ever been read from or canceled", 0)
15+
DEF_ERR(BodyStreamTeeingFailed, JSEXN_ERR, "Cloning body stream failed", 0)
1516
DEF_ERR(InvalidStatus, JSEXN_RANGEERR, "{0}: invalid status {1}", 2)
1617
DEF_ERR(InvalidStreamChunk, JSEXN_TYPEERR, "ReadableStream used as a Request or Response body must produce Uint8Array values", 0)
1718
DEF_ERR(EmptyHeaderName, JSEXN_TYPEERR, "{0}: Header name can't be empty", 1)

builtins/web/fetch/request-response.cpp

Lines changed: 88 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,8 +1074,8 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o
10741074
}
10751075

10761076
JSObject *RequestOrResponse::create_body_stream(JSContext *cx, JS::HandleObject owner) {
1077-
MOZ_ASSERT(is_instance(owner));
10781077
MOZ_ASSERT(!body_stream(owner));
1078+
MOZ_ASSERT(has_body(owner));
10791079
JS::RootedObject source(cx, streams::NativeStreamSource::create(
10801080
cx, owner, JS::UndefinedHandleValue, body_source_pull_algorithm,
10811081
body_source_cancel_algorithm));
@@ -1144,19 +1144,15 @@ JSObject *Request::response_promise(JSObject *obj) {
11441144
.toObject();
11451145
}
11461146

1147-
JSString *Request::method(JSContext *cx, JS::HandleObject obj) {
1147+
JSString *Request::method(HandleObject obj) {
11481148
MOZ_ASSERT(is_instance(obj));
11491149
return JS::GetReservedSlot(obj, static_cast<uint32_t>(Slots::Method)).toString();
11501150
}
11511151

11521152
bool Request::method_get(JSContext *cx, unsigned argc, JS::Value *vp) {
11531153
METHOD_HEADER(0)
11541154

1155-
JSString *method = Request::method(cx, self);
1156-
if (!method)
1157-
return false;
1158-
1159-
args.rval().setString(method);
1155+
args.rval().setString(Request::method(self));
11601156
return true;
11611157
}
11621158

@@ -1197,128 +1193,86 @@ bool Request::bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp) {
11971193

11981194
JSString *GET_atom;
11991195

1200-
// bool Request::clone(JSContext *cx, unsigned argc, JS::Value *vp) {
1201-
// METHOD_HEADER(0);
1202-
1203-
// auto hasBody = RequestOrResponse::has_body(self);
1204-
1205-
// if (hasBody) {
1206-
// if (RequestOrResponse::body_unusable(self)) {
1207-
// return api::throw_error(cx, FetchErrors::BodyStreamUnusable);
1208-
// }
1209-
1210-
// // Here we get the current request's body stream and call ReadableStream.prototype.tee to
1211-
// return
1212-
// // two versions of the stream. Once we get the two streams, we create a new request handle
1213-
// and
1214-
// // attach one of the streams to the new handle and the other stream is attached to the
1215-
// request
1216-
// // handle that `clone()` was called upon.
1217-
// JS::RootedObject body_stream(cx, RequestOrResponse::body_stream(self));
1218-
// if (!body_stream) {
1219-
// body_stream = RequestOrResponse::create_body_stream(cx, self);
1220-
// if (!body_stream) {
1221-
// return false;
1222-
// }
1223-
// }
1224-
// JS::RootedValue tee_val(cx);
1225-
// if (!JS_GetProperty(cx, body_stream, "tee", &tee_val)) {
1226-
// return false;
1227-
// }
1228-
// JS::Rooted<JSFunction *> tee(cx, JS_GetObjectFunction(&tee_val.toObject()));
1229-
// if (!tee) {
1230-
// return false;
1231-
// }
1232-
// JS::RootedVector<JS::Value> argv(cx);
1233-
// JS::RootedValue rval(cx);
1234-
// if (!JS::Call(cx, body_stream, tee, argv, &rval)) {
1235-
// return false;
1236-
// }
1237-
// JS::RootedObject rval_array(cx, &rval.toObject());
1238-
// JS::RootedValue body1_val(cx);
1239-
// if (!JS_GetProperty(cx, rval_array, "0", &body1_val)) {
1240-
// return false;
1241-
// }
1242-
// JS::RootedValue body2_val(cx);
1243-
// if (!JS_GetProperty(cx, rval_array, "1", &body2_val)) {
1244-
// return false;
1245-
// }
1246-
1247-
// auto res = host_api::HttpBody::make(request_handle);
1248-
// if (auto *err = res.to_err()) {
1249-
// HANDLE_ERROR(cx, *err);
1250-
// return false;
1251-
// }
1252-
1253-
// auto body_handle = res.unwrap();
1254-
// if (!JS::IsReadableStream(&body1_val.toObject())) {
1255-
// return false;
1256-
// }
1257-
// body_stream.set(&body1_val.toObject());
1258-
// if (RequestOrResponse::body_unusable(cx, body_stream)) {
1259-
// return api::throw_error(cx, FetchErrors::BodyStreamUnusable);
1260-
// }
1261-
1262-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::Body),
1263-
// JS::Int32Value(body_handle.handle));
1264-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::BodyStream), body1_val);
1265-
1266-
// JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::BodyStream), body2_val);
1267-
// JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::BodyUsed), JS::FalseValue());
1268-
// JS::SetReservedSlot(self, static_cast<uint32_t>(Slots::HasBody), JS::BooleanValue(true));
1269-
// }
1270-
1271-
// JS::RootedObject headers(cx);
1272-
// JS::RootedObject headers_obj(
1273-
// cx, RequestOrResponse::headers(cx, self));
1274-
// if (!headers_obj) {
1275-
// return false;
1276-
// }
1277-
// JS::RootedObject headersInstance(
1278-
// cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj));
1279-
// if (!headersInstance)
1280-
// return false;
1281-
1282-
// headers = Headers::create(cx, headersInstance, Headers::Mode::ProxyToRequest,
1283-
// requestInstance, headers_obj);
1284-
1285-
// if (!headers) {
1286-
// return false;
1287-
// }
1288-
1289-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::Headers),
1290-
// JS::ObjectValue(*headers));
1196+
/// https://fetch.spec.whatwg.org/#dom-request-clone
1197+
bool Request::clone(JSContext *cx, unsigned argc, JS::Value *vp) {
1198+
METHOD_HEADER(0);
1199+
1200+
// clone operation step 1.
1201+
// Let newRequest be a copy of request, except for its body.
1202+
// Note that the spec doesn't say what it means to copy a request, exactly.
1203+
// Since a request only has the fields "method", "url", and "headers", and the "Body" mixin,
1204+
// we copy those three fields in this step.
1205+
RootedObject new_request(cx, create(cx));
1206+
if (!new_request) {
1207+
return false;
1208+
}
1209+
init_slots(new_request);
12911210

1292-
// JSString *method = Request::method(cx, self);
1293-
// if (!method) {
1294-
// return false;
1295-
// }
1211+
RootedValue cloned_headers_val(cx, JS::NullValue());
1212+
RootedObject headers(cx, RequestOrResponse::maybe_headers(self));
1213+
if (headers) {
1214+
RootedValue headers_val(cx, ObjectValue(*headers));
1215+
JSObject *cloned_headers =
1216+
Headers::create(cx, headers_val, host_api::HttpHeadersGuard::Request);
1217+
if (!cloned_headers) {
1218+
return false;
1219+
}
1220+
cloned_headers_val.set(ObjectValue(*cloned_headers));
1221+
} else if (RequestOrResponse::handle(self)) {
1222+
auto handle =
1223+
RequestOrResponse::headers_handle_clone(cx, self, host_api::HttpHeadersGuard::Request);
1224+
JSObject *cloned_headers =
1225+
Headers::create(cx, handle.release(), host_api::HttpHeadersGuard::Request);
1226+
if (!cloned_headers) {
1227+
return false;
1228+
}
1229+
cloned_headers_val.set(ObjectValue(*cloned_headers));
1230+
}
1231+
1232+
SetReservedSlot(new_request, static_cast<uint32_t>(Slots::Headers), cloned_headers_val);
1233+
Value url_val = GetReservedSlot(self, static_cast<uint32_t>(Slots::URL));
1234+
SetReservedSlot(new_request, static_cast<uint32_t>(Slots::URL), url_val);
1235+
Value method_val = JS::StringValue(method(self));
1236+
ENGINE->dump_value(method_val, stderr);
1237+
SetReservedSlot(new_request, static_cast<uint32_t>(Slots::Method), method_val);
1238+
1239+
// clone operation step 2.
1240+
// If request’s body is non-null, set newRequest’s body to the result of cloning request’s body.
1241+
RootedObject new_body(cx);
1242+
auto has_body = RequestOrResponse::has_body(self);
1243+
if (!has_body) {
1244+
args.rval().setObject(*new_request);
1245+
return true;
1246+
}
12961247

1297-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::Method),
1298-
// JS::StringValue(method));
1248+
// Here we get the current request's body stream and call ReadableStream.prototype.tee to
1249+
// get two streams for the same content.
1250+
// One of these is then used to replace the current request's body, the other is used as
1251+
// the body of the clone.
1252+
JS::RootedObject body_stream(cx, RequestOrResponse::body_stream(self));
1253+
if (!body_stream) {
1254+
body_stream = RequestOrResponse::create_body_stream(cx, self);
1255+
if (!body_stream) {
1256+
return false;
1257+
}
1258+
}
12991259

1300-
// auto *request_handle_res = new host_api::HttpOutgoingRequest()
1301-
// if (auto *err = request_handle_res.to_err()) {
1302-
// HANDLE_ERROR(cx, *err);
1303-
// return false;
1304-
// }
1260+
if (RequestOrResponse::body_unusable(cx, body_stream)) {
1261+
return api::throw_error(cx, FetchErrors::BodyStreamUnusable);
1262+
}
13051263

1306-
// auto request_handle = request_handle_res.unwrap();
1264+
RootedObject self_body(cx);
1265+
if (!ReadableStreamTee(cx, body_stream, &self_body, &new_body)) {
1266+
return false;
1267+
}
13071268

1308-
// JS::RootedObject requestInstance(cx, Request::create_instance(cx));
1309-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::Request),
1310-
// JS::Int32Value(request_handle.handle));
1311-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::BodyUsed),
1312-
// JS::FalseValue());
1313-
// JS::SetReservedSlot(
1314-
// requestInstance, static_cast<uint32_t>(Slots::URL),
1315-
// JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::URL)));
1316-
// JS::SetReservedSlot(requestInstance, static_cast<uint32_t>(Slots::HasBody),
1317-
// JS::BooleanValue(hasBody));
1269+
SetReservedSlot(self, static_cast<uint32_t>(Slots::BodyStream), ObjectValue(*self_body));
1270+
SetReservedSlot(new_request, static_cast<uint32_t>(Slots::BodyStream), ObjectValue(*new_body));
1271+
SetReservedSlot(new_request, static_cast<uint32_t>(Slots::HasBody), JS::BooleanValue(true));
13181272

1319-
// args.rval().setObject(*requestInstance);
1320-
// return true;
1321-
// }
1273+
args.rval().setObject(*new_request);
1274+
return true;
1275+
}
13221276

13231277
const JSFunctionSpec Request::static_methods[] = {
13241278
JS_FS_END,
@@ -1333,7 +1287,7 @@ const JSFunctionSpec Request::methods[] = {
13331287
JSPROP_ENUMERATE),
13341288
JS_FN("json", Request::bodyAll<RequestOrResponse::BodyReadResult::JSON>, 0, JSPROP_ENUMERATE),
13351289
JS_FN("text", Request::bodyAll<RequestOrResponse::BodyReadResult::Text>, 0, JSPROP_ENUMERATE),
1336-
// JS_FN("clone", Request::clone, 0, JSPROP_ENUMERATE),
1290+
JS_FN("clone", Request::clone, 0, JSPROP_ENUMERATE),
13371291
JS_FS_END,
13381292
};
13391293

@@ -1372,7 +1326,7 @@ void Request::init_slots(JSObject *requestInstance) {
13721326
* Create a new Request object, roughly according to
13731327
* https://fetch.spec.whatwg.org/#dom-request
13741328
*
1375-
* "Roughly" because not all aspects of Request handling make sense in C@E.
1329+
* "Roughly" because not all aspects of Request handling make sense in StarlingMonkey.
13761330
* The places where we deviate from the spec are called out inline.
13771331
*/
13781332
bool Request::initialize(JSContext *cx, JS::HandleObject request, JS::HandleValue input,
@@ -1416,10 +1370,7 @@ bool Request::initialize(JSContext *cx, JS::HandleObject request, JS::HandleValu
14161370
url_str = RequestOrResponse::url(input_request).toString();
14171371

14181372
// method: `request`’s method.
1419-
method_str = Request::method(cx, input_request);
1420-
if (!method_str) {
1421-
return false;
1422-
}
1373+
method_str = Request::method(input_request);
14231374

14241375
// referrer: `request`’s referrer.
14251376
// TODO: evaluate whether we want to implement support for setting the
@@ -1430,7 +1381,14 @@ bool Request::initialize(JSContext *cx, JS::HandleObject request, JS::HandleValu
14301381

14311382
// header list: A copy of `request`’s header list.
14321383
// Note: copying the headers is postponed, see step 32 below.
1433-
input_headers = RequestOrResponse::maybe_headers(input_request);
1384+
// Note: we're forcing reification of the input request's headers here. That is suboptimal,
1385+
// because we might end up not using them. Additionally, if the headers are represented
1386+
// internally as a handle (e.g. because the input is an incoming request), we would in
1387+
// principle not need to ever reify it just to get a clone.
1388+
// Applying these optimizations is somewhat complex though, so for now we're not doing so.
1389+
if (!(input_headers = RequestOrResponse::headers(cx, input_request))) {
1390+
return false;
1391+
}
14341392

14351393
// The following properties aren't applicable:
14361394
// unsafe-request flag: Set.

builtins/web/fetch/request-response.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class Request final : public BuiltinImpl<Request> {
142142
};
143143

144144
static JSObject *response_promise(JSObject *obj);
145-
static JSString *method(JSContext *cx, JS::HandleObject obj);
145+
static JSString *method(JS::HandleObject obj);
146146
static host_api::HttpRequest *request_handle(JSObject *obj);
147147
static host_api::HttpOutgoingRequest *outgoing_handle(JSObject *obj);
148148
static host_api::HttpIncomingRequest *incoming_handle(JSObject *obj);

tests/integration/fetch/fetch.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { serveTest } from '../test-server.js';
2+
import { strictEqual, deepStrictEqual, throws } from '../assert.js';
3+
4+
export const handler = serveTest(async (t) => {
5+
await t.test('headers-non-ascii-latin1-field-value', async () => {
6+
const response = await fetch("https://http-me.glitch.me/meow?header=cat:é");
7+
strictEqual(response.headers.get('cat'), "é");
8+
});
9+
10+
t.test('request-clone-bad-calls', () => {
11+
throws(() => new Request.prototype.clone(), TypeError);
12+
throws(() => new Request.prototype.clone.call(undefined), TypeError);
13+
});
14+
15+
await t.test('request-clone-valid', async () => {
16+
{
17+
const request = new Request('https://www.fastly.com', {
18+
headers: {
19+
hello: 'world'
20+
},
21+
body: 'te',
22+
method: 'post'
23+
});
24+
const newRequest = request.clone();
25+
strictEqual(newRequest instanceof Request, true, 'newRequest instanceof Request');
26+
strictEqual(newRequest.method, request.method, 'newRequest.method');
27+
strictEqual(newRequest.url, request.url, 'newRequest.url');
28+
deepStrictEqual([...newRequest.headers], [...request.headers], 'newRequest.headers');
29+
strictEqual(request.bodyUsed, false, 'request.bodyUsed');
30+
strictEqual(newRequest.bodyUsed, false, 'newRequest.bodyUsed');
31+
strictEqual(newRequest.body instanceof ReadableStream, true, 'newRequest.body instanceof ReadableStream');
32+
}
33+
34+
{
35+
const request = new Request('https://www.fastly.com', {
36+
method: 'get'
37+
})
38+
const newRequest = request.clone();
39+
40+
strictEqual(newRequest.bodyUsed, false, 'newRequest.bodyUsed');
41+
strictEqual(newRequest.body, null, 'newRequest.body');
42+
}
43+
});
44+
45+
await t.test('request-clone-invalid', async () => {
46+
const request = new Request('https://www.fastly.com', {
47+
headers: {
48+
hello: 'world'
49+
},
50+
body: 'te',
51+
method: 'post'
52+
});
53+
await request.text();
54+
throws(() => request.clone());
55+
});
56+
});

tests/integration/handlers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export { handler as btoa } from './btoa/btoa.js';
22
export { handler as performance } from './performance/performance.js';
33
export { handler as crypto } from './crypto/crypto.js';
44
export { handler as timers } from './timers/timers.js';
5-
export { handler as headers } from './headers/headers.js';
5+
export { handler as fetch } from './fetch/fetch.js';

tests/integration/headers/headers.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/tests.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ test_e2e(tla-runtime-resolve)
3838
test_integration(btoa)
3939
test_integration(performance)
4040
test_integration(crypto)
41-
test_integration(headers)
41+
test_integration(fetch)
4242
test_integration(timers)

0 commit comments

Comments
 (0)