Skip to content

Commit 1623d74

Browse files
Jake ChampionJakeChampion
authored andcommitted
feat: implement Response.redirect static method and Response.prototype.redirected getter
1 parent e2bb2ee commit 1623d74

File tree

9 files changed

+243
-11
lines changed

9 files changed

+243
-11
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ jobs:
248248
- request-upstream
249249
- response
250250
- response-headers
251+
- response-redirect
251252
- secret-store
252253
- status
253254
- streaming-close
@@ -395,6 +396,7 @@ jobs:
395396
- 'request-upstream'
396397
- 'response'
397398
- 'response-headers'
399+
- 'response-redirect'
398400
- 'secret-store'
399401
- 'status'
400402
- 'timers'

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "js/JSON.h"
1818
#include "js/Stream.h"
1919
#include <algorithm>
20+
#include <iostream>
2021
#include <vector>
2122

2223
#pragma clang diagnostic push
@@ -2236,6 +2237,14 @@ bool Response::type_get(JSContext *cx, unsigned argc, JS::Value *vp) {
22362237
return true;
22372238
}
22382239

2240+
bool Response::redirected_get(JSContext *cx, unsigned argc, JS::Value *vp) {
2241+
METHOD_HEADER(0)
2242+
2243+
args.rval().setBoolean(
2244+
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::Redirected)).toBoolean());
2245+
return true;
2246+
}
2247+
22392248
bool Response::headers_get(JSContext *cx, unsigned argc, JS::Value *vp) {
22402249
METHOD_HEADER(0)
22412250

@@ -2265,7 +2274,118 @@ bool Response::bodyUsed_get(JSContext *cx, unsigned argc, JS::Value *vp) {
22652274
return true;
22662275
}
22672276

2277+
// https://fetch.spec.whatwg.org/#dom-response-redirect
2278+
// [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
2279+
bool Response::redirect(JSContext *cx, unsigned argc, JS::Value *vp) {
2280+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
2281+
if (!args.requireAtLeast(cx, "redirect", 1)) {
2282+
return false;
2283+
}
2284+
auto url = args.get(0);
2285+
// 1. Let parsedURL be the result of parsing url with current settings object’s API base URL.
2286+
JS::RootedObject urlInstance(
2287+
cx, JS_NewObjectWithGivenProto(cx, &builtins::URL::class_, builtins::URL::proto_obj));
2288+
if (!urlInstance) {
2289+
return false;
2290+
}
2291+
JS::RootedObject parsedURL(
2292+
cx, builtins::URL::create(cx, urlInstance, url, builtins::Fastly::baseURL));
2293+
// 2. If parsedURL is failure, then throw a TypeError.
2294+
if (!parsedURL) {
2295+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RESPONSE_REDIRECT_INVALID_URI);
2296+
return false;
2297+
}
2298+
JS::RootedValue url_val(cx, JS::ObjectValue(*parsedURL));
2299+
size_t length;
2300+
auto url_str = encode(cx, url_val, &length);
2301+
if (!url_str) {
2302+
return false;
2303+
}
2304+
auto value = url_str.get();
2305+
// 3. If status is not a redirect status, then throw a RangeError.
2306+
// A redirect status is a status that is 301, 302, 303, 307, or 308.
2307+
auto statusVal = args.get(1);
2308+
uint16_t status;
2309+
if (statusVal.isUndefined()) {
2310+
status = 302;
2311+
} else {
2312+
if (!JS::ToUint16(cx, statusVal, &status)) {
2313+
return false;
2314+
}
2315+
}
2316+
if (status != 301 && status != 302 && status != 303 && status != 307 && status != 308) {
2317+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RESPONSE_REDIRECT_INVALID_STATUS);
2318+
return false;
2319+
}
2320+
// 4. Let responseObject be the result of creating a Response object, given a new response,
2321+
// "immutable", and this’s relevant Realm.
2322+
fastly_response_handle_t response_handle = INVALID_HANDLE;
2323+
fastly_error_t err;
2324+
if (!fastly_http_resp_new(&response_handle, &err)) {
2325+
HANDLE_ERROR(cx, err);
2326+
return false;
2327+
}
2328+
if (response_handle == INVALID_HANDLE) {
2329+
return false;
2330+
}
2331+
2332+
auto make_res = HttpBody::make();
2333+
if (auto *err = make_res.to_err()) {
2334+
HANDLE_ERROR(cx, *err);
2335+
return false;
2336+
}
2337+
2338+
auto body = make_res.unwrap();
2339+
JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, &builtins::Response::class_,
2340+
builtins::Response::proto_obj));
2341+
if (!response_instance) {
2342+
return false;
2343+
}
2344+
JS::RootedObject response(cx, create(cx, response_instance, response_handle, body.handle, false));
2345+
if (!response) {
2346+
return false;
2347+
}
2348+
2349+
// 5. Set responseObject’s response’s status to status.
2350+
if (!fastly_http_resp_status_set(response_handle, status, &err)) {
2351+
HANDLE_ERROR(cx, err);
2352+
return false;
2353+
}
2354+
// To ensure that we really have the same status value as the host,
2355+
// we always read it back here.
2356+
if (!fastly_http_resp_status_get(response_handle, &status, &err)) {
2357+
HANDLE_ERROR(cx, err);
2358+
return false;
2359+
}
2360+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Status), JS::Int32Value(status));
2361+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::StatusMessage),
2362+
JS::StringValue(JS_GetEmptyString(cx)));
2363+
// 6. Let value be parsedURL, serialized and isomorphic encoded.
2364+
// 7. Append (`Location`, value) to responseObject’s response’s header list.
2365+
JS::RootedObject headers(cx);
2366+
JS::RootedObject headersInstance(
2367+
cx, JS_NewObjectWithGivenProto(cx, &builtins::Headers::class_, builtins::Headers::proto_obj));
2368+
if (!headersInstance)
2369+
return false;
2370+
2371+
headers = builtins::Headers::create(cx, headersInstance, builtins::Headers::Mode::ProxyToResponse,
2372+
response);
2373+
if (!headers) {
2374+
return false;
2375+
}
2376+
if (!builtins::Headers::maybe_add(cx, headers, "Location", value)) {
2377+
return false;
2378+
}
2379+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::ObjectValue(*headers));
2380+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
2381+
// 8. Return responseObject.
2382+
2383+
args.rval().setObjectOrNull(response);
2384+
return true;
2385+
}
2386+
22682387
const JSFunctionSpec Response::static_methods[] = {
2388+
JS_FN("redirect", redirect, 1, JSPROP_ENUMERATE),
22692389
JS_FS_END,
22702390
};
22712391

@@ -2282,6 +2402,7 @@ const JSFunctionSpec Response::methods[] = {
22822402
};
22832403

22842404
const JSPropertySpec Response::properties[] = {
2405+
JS_PSG("redirected", redirected_get, JSPROP_ENUMERATE),
22852406
JS_PSG("type", type_get, JSPROP_ENUMERATE),
22862407
JS_PSG("url", url_get, JSPROP_ENUMERATE),
22872408
JS_PSG("status", status_get, JSPROP_ENUMERATE),
@@ -2458,13 +2579,18 @@ bool Response::init_class(JSContext *cx, JS::HandleObject global) {
24582579
JSObject *Response::create(JSContext *cx, JS::HandleObject response,
24592580
fastly_response_handle_t response_handle,
24602581
fastly_body_handle_t body_handle, bool is_upstream) {
2582+
// MOZ_ASSERT(cx);
2583+
// MOZ_ASSERT(is_instance(response));
2584+
// MOZ_ASSERT(response_handle);
2585+
// MOZ_ASSERT(body_handle);
24612586
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Response),
24622587
JS::Int32Value(response_handle));
24632588
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::NullValue());
24642589
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Body), JS::Int32Value(body_handle));
24652590
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::BodyStream), JS::NullValue());
24662591
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::HasBody), JS::FalseValue());
24672592
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::BodyUsed), JS::FalseValue());
2593+
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
24682594
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::IsUpstream),
24692595
JS::BooleanValue(is_upstream));
24702596

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ class Response final : public BuiltinImpl<Response> {
172172
static bool version_get(JSContext *cx, unsigned argc, JS::Value *vp);
173173
static bool type_get(JSContext *cx, unsigned argc, JS::Value *vp);
174174
static bool headers_get(JSContext *cx, unsigned argc, JS::Value *vp);
175-
static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
175+
static bool redirected_get(JSContext *cx, unsigned argc, JS::Value *vp);
176176

177177
template <RequestOrResponse::BodyReadResult result_type>
178178
static bool bodyAll(JSContext *cx, unsigned argc, JS::Value *vp);
@@ -192,6 +192,7 @@ class Response final : public BuiltinImpl<Response> {
192192
IsUpstream = static_cast<int>(RequestOrResponse::Slots::Count),
193193
Status,
194194
StatusMessage,
195+
Redirected,
195196
Count,
196197
};
197198
static const JSFunctionSpec static_methods[];
@@ -204,6 +205,7 @@ class Response final : public BuiltinImpl<Response> {
204205
static bool init_class(JSContext *cx, JS::HandleObject global);
205206
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
206207

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

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,6 @@ MSG_DEF(JSMSG_REQUEST_BACKEND_DOES_NOT_EXIST, 1, JSEXN_TYPEERR,
103103
MSG_DEF(JSMSG_SUBTLE_CRYPTO_ERROR, 1, JSEXN_ERR, "{0}")
104104
MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_JWK_KTY_VALUE, 1, JSEXN_ERR, "The JWK 'kty' member was not '{0}'")
105105
MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_KEY_USAGES_VALUE, 0, JSEXN_TYPEERR, "Invalid keyUsages argument")
106+
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_URI, 0, JSEXN_TYPEERR, "Response.redirect: url parameter is not a valid URL.")
107+
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_STATUS, 0, JSEXN_RANGEERR, "Response.redirect: Invalid redirect status code.")
106108
//clang-format on
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-env serviceworker */
2+
import { pass, assert, assertThrows } from "../../../assertions.js";
3+
import { routes } from "./test-harness.js";
4+
5+
routes.set("/response/redirect", async () => {
6+
const url = "http://test.url:1234/";
7+
const redirectResponse = Response.redirect(url);
8+
let error = assert(redirectResponse.type, "default");
9+
if (error) { return error; }
10+
error = assert(redirectResponse.redirected, false);
11+
if (error) { return error; }
12+
error = assert(redirectResponse.ok, false);
13+
if (error) { return error; }
14+
error = assert(redirectResponse.status, 302, "Default redirect status is 302");
15+
if (error) { return error; }
16+
error = assert(redirectResponse.headers.get("Location"), url)
17+
if (error) { return error; }
18+
error = assert(redirectResponse.statusText, "");
19+
if (error) { return error; }
20+
21+
for (const status of [301, 302, 303, 307, 308]) {
22+
const redirectResponse = Response.redirect(url, status);
23+
error = assert(redirectResponse.type, "default");
24+
if (error) { return error; }
25+
error = assert(redirectResponse.redirected, false);
26+
if (error) { return error; }
27+
error = assert(redirectResponse.ok, false);
28+
if (error) { return error; }
29+
error = assert(redirectResponse.status, status, "Redirect status is " + status);
30+
if (error) { return error; }
31+
error = assert(redirectResponse.headers.get("Location"), url);
32+
if (error) { return error; }
33+
error = assert(redirectResponse.statusText, "");
34+
if (error) { return error; }
35+
}
36+
const invalidUrl = "http://:This is not an url";
37+
error = assertThrows(function () { Response.redirect(invalidUrl); }, TypeError);
38+
if (error) { return error; }
39+
for (const invalidStatus of [200, 309, 400, 500]) {
40+
error = assertThrows(function () { Response.redirect(url, invalidStatus); }, RangeError);
41+
if (error) { return error; }
42+
}
43+
return pass()
44+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* eslint-env serviceworker */
2+
import { env } from 'fastly:env';
3+
import { fail } from "../../../assertions.js";
4+
5+
addEventListener("fetch", event => {
6+
event.respondWith(app(event))
7+
})
8+
/**
9+
* @param {FetchEvent} event
10+
* @returns {Response}
11+
*/
12+
async 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 await 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+
export 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+
})
Lines changed: 12 additions & 0 deletions
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-redirect"
9+
service_id = ""
10+
11+
[scripts]
12+
build = "node ../../../../js-compute-runtime-cli.js"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"GET /response/redirect": {
3+
"environments": ["viceroy", "c@e"],
4+
"downstream_request": {
5+
"method": "GET",
6+
"pathname": "/response/redirect"
7+
},
8+
"downstream_response": {
9+
"status": 200
10+
}
11+
}
12+
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
{
22
"Check default redirect response": {
3-
"status": "FAIL"
3+
"status": "PASS"
44
},
55
"Check response returned by static method redirect(), status = 301": {
6-
"status": "FAIL"
6+
"status": "PASS"
77
},
88
"Check response returned by static method redirect(), status = 302": {
9-
"status": "FAIL"
9+
"status": "PASS"
1010
},
1111
"Check response returned by static method redirect(), status = 303": {
12-
"status": "FAIL"
12+
"status": "PASS"
1313
},
1414
"Check response returned by static method redirect(), status = 307": {
15-
"status": "FAIL"
15+
"status": "PASS"
1616
},
1717
"Check response returned by static method redirect(), status = 308": {
18-
"status": "FAIL"
18+
"status": "PASS"
1919
},
2020
"Check error returned when giving invalid url to redirect()": {
2121
"status": "PASS"
2222
},
2323
"Check error returned when giving invalid status to redirect(), status = 200": {
24-
"status": "FAIL"
24+
"status": "PASS"
2525
},
2626
"Check error returned when giving invalid status to redirect(), status = 309": {
27-
"status": "FAIL"
27+
"status": "PASS"
2828
},
2929
"Check error returned when giving invalid status to redirect(), status = 400": {
30-
"status": "FAIL"
30+
"status": "PASS"
3131
},
3232
"Check error returned when giving invalid status to redirect(), status = 500": {
33-
"status": "FAIL"
33+
"status": "PASS"
3434
}
3535
}

0 commit comments

Comments
 (0)