Skip to content

Commit 780067d

Browse files
feat: implement Response.json static method (#499)
Co-authored-by: Trevor Elliott <[email protected]>
1 parent 1a9be1c commit 780067d

File tree

11 files changed

+313
-4
lines changed

11 files changed

+313
-4
lines changed

.github/workflows/main.yml

+2
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ jobs:
248248
- request-upstream
249249
- response
250250
- response-headers
251+
- response-json
251252
- response-redirect
252253
- secret-store
253254
- status
@@ -396,6 +397,7 @@ jobs:
396397
- 'request-upstream'
397398
- 'response'
398399
- 'response-headers'
400+
- 'response-json'
399401
- 'response-redirect'
400402
- 'secret-store'
401403
- 'status'

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

+153-1
Original file line numberDiff line numberDiff line change
@@ -2373,7 +2373,7 @@ bool Response::redirect(JSContext *cx, unsigned argc, JS::Value *vp) {
23732373
if (!headers) {
23742374
return false;
23752375
}
2376-
if (!builtins::Headers::maybe_add(cx, headers, "Location", value)) {
2376+
if (!builtins::Headers::maybe_add(cx, headers, "location", value)) {
23772377
return false;
23782378
}
23792379
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::ObjectValue(*headers));
@@ -2384,8 +2384,160 @@ bool Response::redirect(JSContext *cx, unsigned argc, JS::Value *vp) {
23842384
return true;
23852385
}
23862386

2387+
namespace {
2388+
bool callbackCalled;
2389+
bool write_json_to_buf(const char16_t *str, uint32_t strlen, void *out) {
2390+
callbackCalled = true;
2391+
auto outstr = static_cast<std::u16string *>(out);
2392+
outstr->append(str, strlen);
2393+
2394+
return true;
2395+
}
2396+
} // namespace
2397+
2398+
bool Response::json(JSContext *cx, unsigned argc, JS::Value *vp) {
2399+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
2400+
if (!args.requireAtLeast(cx, "json", 1)) {
2401+
return false;
2402+
}
2403+
JS::RootedValue data(cx, args.get(0));
2404+
JS::RootedValue init_val(cx, args.get(1));
2405+
JS::RootedObject replacer(cx);
2406+
JS::RootedValue space(cx);
2407+
2408+
std::u16string out;
2409+
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
2410+
callbackCalled = false;
2411+
if (!JS::ToJSON(cx, data, replacer, space, &write_json_to_buf, &out)) {
2412+
return false;
2413+
}
2414+
if (!callbackCalled) {
2415+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RESPONSE_JSON_INVALID_VALUE);
2416+
return false;
2417+
}
2418+
// 2. Let body be the result of extracting bytes.
2419+
2420+
// 3. Let responseObject be the result of creating a Response object, given a new response,
2421+
// "response", and this’s relevant Realm.
2422+
JS::RootedValue status_val(cx);
2423+
uint16_t status = 200;
2424+
2425+
JS::RootedValue statusText_val(cx);
2426+
JS::RootedString statusText(cx, JS_GetEmptyString(cx));
2427+
JS::RootedValue headers_val(cx);
2428+
2429+
if (init_val.isObject()) {
2430+
JS::RootedObject init(cx, init_val.toObjectOrNull());
2431+
if (!JS_GetProperty(cx, init, "status", &status_val) ||
2432+
!JS_GetProperty(cx, init, "statusText", &statusText_val) ||
2433+
!JS_GetProperty(cx, init, "headers", &headers_val)) {
2434+
return false;
2435+
}
2436+
2437+
if (!status_val.isUndefined() && !JS::ToUint16(cx, status_val, &status)) {
2438+
return false;
2439+
}
2440+
2441+
if (status == 204 || status == 205 || status == 304) {
2442+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
2443+
JSMSG_RESPONSE_NULL_BODY_STATUS_WITH_BODY);
2444+
return false;
2445+
}
2446+
2447+
if (!statusText_val.isUndefined() && !(statusText = JS::ToString(cx, statusText_val))) {
2448+
return false;
2449+
}
2450+
2451+
} else if (!init_val.isNullOrUndefined()) {
2452+
JS_ReportErrorLatin1(cx, "Response constructor: |init| parameter can't be converted to "
2453+
"a dictionary");
2454+
return false;
2455+
}
2456+
2457+
fastly_response_handle_t response_handle = INVALID_HANDLE;
2458+
fastly_error_t err;
2459+
if (!fastly_http_resp_new(&response_handle, &err)) {
2460+
HANDLE_ERROR(cx, err);
2461+
return false;
2462+
}
2463+
if (response_handle == INVALID_HANDLE) {
2464+
return false;
2465+
}
2466+
2467+
auto make_res = HttpBody::make();
2468+
if (auto *err = make_res.to_err()) {
2469+
HANDLE_ERROR(cx, *err);
2470+
return false;
2471+
}
2472+
2473+
auto body = make_res.unwrap();
2474+
JS::RootedString string(cx, JS_NewUCStringCopyN(cx, out.c_str(), out.length()));
2475+
size_t encoded_len;
2476+
auto stringChars = encode(cx, string, &encoded_len);
2477+
2478+
auto write_res = body.write_all(reinterpret_cast<uint8_t *>(stringChars.get()), encoded_len);
2479+
if (auto *err = write_res.to_err()) {
2480+
HANDLE_ERROR(cx, *err);
2481+
return false;
2482+
}
2483+
JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, &builtins::Response::class_,
2484+
builtins::Response::proto_obj));
2485+
if (!response_instance) {
2486+
return false;
2487+
}
2488+
JS::RootedObject response(cx, create(cx, response_instance, response_handle, body.handle, false));
2489+
if (!response) {
2490+
return false;
2491+
}
2492+
2493+
// Set `this`’s `response`’s `status` to `init`["status"].
2494+
if (!fastly_http_resp_status_set(response_handle, status, &err)) {
2495+
HANDLE_ERROR(cx, err);
2496+
return false;
2497+
}
2498+
// To ensure that we really have the same status value as the host,
2499+
// we always read it back here.
2500+
if (!fastly_http_resp_status_get(response_handle, &status, &err)) {
2501+
HANDLE_ERROR(cx, err);
2502+
return false;
2503+
}
2504+
2505+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Status), JS::Int32Value(status));
2506+
2507+
// Set `this`’s `response`’s `status message` to `init`["statusText"].
2508+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::StatusMessage),
2509+
JS::StringValue(statusText));
2510+
2511+
// If `init`["headers"] `exists`, then `fill` `this`’s `headers` with
2512+
// `init`["headers"].
2513+
JS::RootedObject headers(cx);
2514+
JS::RootedObject headersInstance(
2515+
cx, JS_NewObjectWithGivenProto(cx, &builtins::Headers::class_, builtins::Headers::proto_obj));
2516+
if (!headersInstance)
2517+
return false;
2518+
2519+
headers = builtins::Headers::create(cx, headersInstance, builtins::Headers::Mode::ProxyToResponse,
2520+
response, headers_val);
2521+
if (!headers) {
2522+
return false;
2523+
}
2524+
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
2525+
if (!builtins::Headers::maybe_add(cx, headers, "content-type", "application/json")) {
2526+
return false;
2527+
}
2528+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::ObjectValue(*headers));
2529+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
2530+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::HasBody), JS::TrueValue());
2531+
RequestOrResponse::set_url(response, JS_GetEmptyStringValue(cx));
2532+
2533+
// 5. Return responseObject.
2534+
args.rval().setObjectOrNull(response);
2535+
return true;
2536+
}
2537+
23872538
const JSFunctionSpec Response::static_methods[] = {
23882539
JS_FN("redirect", redirect, 1, JSPROP_ENUMERATE),
2540+
JS_FN("json", json, 1, JSPROP_ENUMERATE),
23892541
JS_FS_END,
23902542
};
23912543

c-dependencies/js-compute-runtime/builtins/request-response.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ class Response final : public BuiltinImpl<Response> {
179179
static bool body_get(JSContext *cx, unsigned argc, JS::Value *vp);
180180
static bool bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp);
181181

182+
static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
183+
static bool json(JSContext *cx, unsigned argc, JS::Value *vp);
184+
182185
public:
183186
static constexpr const char *class_name = "Response";
184187

@@ -205,7 +208,6 @@ class Response final : public BuiltinImpl<Response> {
205208
static bool init_class(JSContext *cx, JS::HandleObject global);
206209
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
207210

208-
static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
209211
static JSObject *create(JSContext *cx, JS::HandleObject response,
210212
fastly_response_handle_t response_handle,
211213
fastly_body_handle_t body_handle, bool is_upstream);

c-dependencies/js-compute-runtime/error-numbers.msg

+2
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_JWK_KTY_VALUE, 1, JSEXN_ERR, "Th
105105
MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_KEY_USAGES_VALUE, 0, JSEXN_TYPEERR, "Invalid keyUsages argument")
106106
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_URI, 0, JSEXN_TYPEERR, "Response.redirect: url parameter is not a valid URL.")
107107
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_STATUS, 0, JSEXN_RANGEERR, "Response.redirect: Invalid redirect status code.")
108+
MSG_DEF(JSMSG_RESPONSE_NULL_BODY_STATUS_WITH_BODY, 0, JSEXN_TYPEERR, "Response with null body status cannot have body")
109+
MSG_DEF(JSMSG_RESPONSE_JSON_INVALID_VALUE, 0, JSEXN_TYPEERR, "Redirect.json: The data is not JSON serializable")
108110
//clang-format on
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint-env serviceworker */
2+
import { pass, assert, assertThrows } from "../../../assertions.js";
3+
import { routes } from "../../../test-harness.js";
4+
5+
let error;
6+
routes.set("/response/json", async () => {
7+
const APPLICATION_JSON = "application/json";
8+
const FOO_BAR = "foo/bar";
9+
10+
const INIT_TESTS = [
11+
[undefined, 200, "", APPLICATION_JSON, {}],
12+
[{ status: 400 }, 400, "", APPLICATION_JSON, {}],
13+
[{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}],
14+
[{ headers: {} }, 200, "", APPLICATION_JSON, {}],
15+
[{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}],
16+
[{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }],
17+
];
18+
19+
for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
20+
const response = Response.json("hello world", init);
21+
error = assert(response.type, "default", 'response.type');
22+
if (error) { return error; }
23+
error = assert(response.status, expectedStatus, 'response.status');
24+
if (error) { return error; }
25+
error = assert(response.statusText, expectedStatusText, 'response.statusText');
26+
if (error) { return error; }
27+
error = assert(response.headers.get("content-type"), expectedContentType, 'response.headers.get("content-type")');
28+
if (error) { return error; }
29+
for (const key in expectedHeaders) {
30+
error = assert(response.headers.get(key), expectedHeaders[key], 'response.headers.get(key)');
31+
if (error) { return error; }
32+
}
33+
const data = await response.json();
34+
error = assert(data, "hello world", 'data');
35+
if (error) { return error; }
36+
}
37+
38+
const nullBodyStatus = [204, 205, 304];
39+
for (const status of nullBodyStatus) {
40+
error = assertThrows(
41+
function () {
42+
Response.json("hello world", { status: status });
43+
},
44+
TypeError,
45+
);
46+
if (error) { return error; }
47+
}
48+
49+
const response = Response.json({ foo: "bar" });
50+
const data = await response.json();
51+
error = assert(typeof data, "object", 'typeof data');
52+
if (error) { return error; }
53+
error = assert(data.foo, "bar", "data.foo");
54+
if (error) { return error; }
55+
56+
error = assertThrows(
57+
function () {
58+
Response.json(Symbol("foo"));
59+
},
60+
TypeError
61+
);
62+
if (error) { return error; }
63+
64+
const a = { b: 1 };
65+
a.a = a;
66+
error = assertThrows(
67+
function () {
68+
Response.json(a);
69+
},
70+
TypeError,
71+
);
72+
if (error) { return error; }
73+
74+
class CustomError extends Error {
75+
name = "CustomError";
76+
}
77+
error = assertThrows(
78+
function () {
79+
Response.json({ get foo() { throw new CustomError("bar") } });
80+
},
81+
CustomError,
82+
)
83+
if (error) { return error; }
84+
return pass()
85+
});
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 = "response-json"
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,12 @@
1+
{
2+
"GET /response/json": {
3+
"environments": ["viceroy", "c@e"],
4+
"downstream_request": {
5+
"method": "GET",
6+
"pathname": "/response/json"
7+
},
8+
"downstream_response": {
9+
"status": 200
10+
}
11+
}
12+
}

integration-tests/js-compute/fixtures/response-redirect/bin/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-env serviceworker */
22
import { pass, assert, assertThrows } from "../../../assertions.js";
3-
import { routes } from "./test-harness.js";
3+
import { routes } from "../../../test-harness.js";
44

55
routes.set("/response/redirect", async () => {
66
const url = "http://test.url:1234/";
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-env serviceworker */
22
import { env } from 'fastly:env';
3-
import { fail } from "../../../assertions.js";
3+
import { fail } from "./assertions.js";
44

55
addEventListener("fetch", event => {
66
event.respondWith(app(event))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"Check response returned by static json() with init undefined": {
3+
"status": "PASS"
4+
},
5+
"Check response returned by static json() with init {\"status\":400}": {
6+
"status": "PASS"
7+
},
8+
"Check response returned by static json() with init {\"statusText\":\"foo\"}": {
9+
"status": "PASS"
10+
},
11+
"Check response returned by static json() with init {\"headers\":{}}": {
12+
"status": "PASS"
13+
},
14+
"Check response returned by static json() with init {\"headers\":{\"content-type\":\"foo/bar\"}}": {
15+
"status": "PASS"
16+
},
17+
"Check response returned by static json() with init {\"headers\":{\"x-foo\":\"bar\"}}": {
18+
"status": "PASS"
19+
},
20+
"Throws TypeError when calling static json() with a status of 204": {
21+
"status": "PASS"
22+
},
23+
"Throws TypeError when calling static json() with a status of 205": {
24+
"status": "PASS"
25+
},
26+
"Throws TypeError when calling static json() with a status of 304": {
27+
"status": "PASS"
28+
},
29+
"Check static json() encodes JSON objects correctly": {
30+
"status": "PASS"
31+
},
32+
"Check static json() throws when data is not encodable": {
33+
"status": "PASS"
34+
},
35+
"Check static json() throws when data is circular": {
36+
"status": "PASS"
37+
},
38+
"Check static json() propagates JSON serializer errors": {
39+
"status": "PASS"
40+
}
41+
}

tests/wpt-harness/tests.json

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"fetch/api/response/response-init-002.any.js",
9898
"fetch/api/response/response-init-contenttype.any.js",
9999
"fetch/api/response/response-static-error.any.js",
100+
"fetch/api/response/response-static-json.any.js",
100101
"fetch/api/response/response-static-redirect.any.js",
101102
"fetch/api/response/response-stream-disturbed-1.any.js",
102103
"fetch/api/response/response-stream-disturbed-2.any.js",

0 commit comments

Comments
 (0)