Skip to content

Commit

Permalink
feat: implement Response.redirect static method and Response.prototyp…
Browse files Browse the repository at this point in the history
…e.redirected getter
  • Loading branch information
JakeChampion committed Apr 19, 2023
1 parent e2bb2ee commit 1623d74
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Expand Up @@ -248,6 +248,7 @@ jobs:
- request-upstream
- response
- response-headers
- response-redirect
- secret-store
- status
- streaming-close
Expand Down Expand Up @@ -395,6 +396,7 @@ jobs:
- 'request-upstream'
- 'response'
- 'response-headers'
- 'response-redirect'
- 'secret-store'
- 'status'
- 'timers'
Expand Down
126 changes: 126 additions & 0 deletions c-dependencies/js-compute-runtime/builtins/request-response.cpp
Expand Up @@ -17,6 +17,7 @@
#include "js/JSON.h"
#include "js/Stream.h"
#include <algorithm>
#include <iostream>
#include <vector>

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

bool Response::redirected_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

args.rval().setBoolean(
JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::Redirected)).toBoolean());
return true;
}

bool Response::headers_get(JSContext *cx, unsigned argc, JS::Value *vp) {
METHOD_HEADER(0)

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

// https://fetch.spec.whatwg.org/#dom-response-redirect
// [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
bool Response::redirect(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "redirect", 1)) {
return false;
}
auto url = args.get(0);
// 1. Let parsedURL be the result of parsing url with current settings object’s API base URL.
JS::RootedObject urlInstance(
cx, JS_NewObjectWithGivenProto(cx, &builtins::URL::class_, builtins::URL::proto_obj));
if (!urlInstance) {
return false;
}
JS::RootedObject parsedURL(
cx, builtins::URL::create(cx, urlInstance, url, builtins::Fastly::baseURL));
// 2. If parsedURL is failure, then throw a TypeError.
if (!parsedURL) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RESPONSE_REDIRECT_INVALID_URI);
return false;
}
JS::RootedValue url_val(cx, JS::ObjectValue(*parsedURL));
size_t length;
auto url_str = encode(cx, url_val, &length);
if (!url_str) {
return false;
}
auto value = url_str.get();
// 3. If status is not a redirect status, then throw a RangeError.
// A redirect status is a status that is 301, 302, 303, 307, or 308.
auto statusVal = args.get(1);
uint16_t status;
if (statusVal.isUndefined()) {
status = 302;
} else {
if (!JS::ToUint16(cx, statusVal, &status)) {
return false;
}
}
if (status != 301 && status != 302 && status != 303 && status != 307 && status != 308) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_RESPONSE_REDIRECT_INVALID_STATUS);
return false;
}
// 4. Let responseObject be the result of creating a Response object, given a new response,
// "immutable", and this’s relevant Realm.
fastly_response_handle_t response_handle = INVALID_HANDLE;
fastly_error_t err;
if (!fastly_http_resp_new(&response_handle, &err)) {
HANDLE_ERROR(cx, err);
return false;
}
if (response_handle == INVALID_HANDLE) {
return false;
}

auto make_res = HttpBody::make();
if (auto *err = make_res.to_err()) {
HANDLE_ERROR(cx, *err);
return false;
}

auto body = make_res.unwrap();
JS::RootedObject response_instance(cx, JS_NewObjectWithGivenProto(cx, &builtins::Response::class_,
builtins::Response::proto_obj));
if (!response_instance) {
return false;
}
JS::RootedObject response(cx, create(cx, response_instance, response_handle, body.handle, false));
if (!response) {
return false;
}

// 5. Set responseObject’s response’s status to status.
if (!fastly_http_resp_status_set(response_handle, status, &err)) {
HANDLE_ERROR(cx, err);
return false;
}
// To ensure that we really have the same status value as the host,
// we always read it back here.
if (!fastly_http_resp_status_get(response_handle, &status, &err)) {
HANDLE_ERROR(cx, err);
return false;
}
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Status), JS::Int32Value(status));
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::StatusMessage),
JS::StringValue(JS_GetEmptyString(cx)));
// 6. Let value be parsedURL, serialized and isomorphic encoded.
// 7. Append (`Location`, value) to responseObject’s response’s header list.
JS::RootedObject headers(cx);
JS::RootedObject headersInstance(
cx, JS_NewObjectWithGivenProto(cx, &builtins::Headers::class_, builtins::Headers::proto_obj));
if (!headersInstance)
return false;

headers = builtins::Headers::create(cx, headersInstance, builtins::Headers::Mode::ProxyToResponse,
response);
if (!headers) {
return false;
}
if (!builtins::Headers::maybe_add(cx, headers, "Location", value)) {
return false;
}
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::ObjectValue(*headers));
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
// 8. Return responseObject.

args.rval().setObjectOrNull(response);
return true;
}

const JSFunctionSpec Response::static_methods[] = {
JS_FN("redirect", redirect, 1, JSPROP_ENUMERATE),
JS_FS_END,
};

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

const JSPropertySpec Response::properties[] = {
JS_PSG("redirected", redirected_get, JSPROP_ENUMERATE),
JS_PSG("type", type_get, JSPROP_ENUMERATE),
JS_PSG("url", url_get, JSPROP_ENUMERATE),
JS_PSG("status", status_get, JSPROP_ENUMERATE),
Expand Down Expand Up @@ -2458,13 +2579,18 @@ bool Response::init_class(JSContext *cx, JS::HandleObject global) {
JSObject *Response::create(JSContext *cx, JS::HandleObject response,
fastly_response_handle_t response_handle,
fastly_body_handle_t body_handle, bool is_upstream) {
// MOZ_ASSERT(cx);
// MOZ_ASSERT(is_instance(response));
// MOZ_ASSERT(response_handle);
// MOZ_ASSERT(body_handle);
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Response),
JS::Int32Value(response_handle));
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Headers), JS::NullValue());
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Body), JS::Int32Value(body_handle));
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::BodyStream), JS::NullValue());
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::HasBody), JS::FalseValue());
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::BodyUsed), JS::FalseValue());
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::Redirected), JS::FalseValue());
JS::SetReservedSlot(response, static_cast<uint32_t>(Slots::IsUpstream),
JS::BooleanValue(is_upstream));

Expand Down
Expand Up @@ -172,7 +172,7 @@ class Response final : public BuiltinImpl<Response> {
static bool version_get(JSContext *cx, unsigned argc, JS::Value *vp);
static bool type_get(JSContext *cx, unsigned argc, JS::Value *vp);
static bool headers_get(JSContext *cx, unsigned argc, JS::Value *vp);
static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
static bool redirected_get(JSContext *cx, unsigned argc, JS::Value *vp);

template <RequestOrResponse::BodyReadResult result_type>
static bool bodyAll(JSContext *cx, unsigned argc, JS::Value *vp);
Expand All @@ -192,6 +192,7 @@ class Response final : public BuiltinImpl<Response> {
IsUpstream = static_cast<int>(RequestOrResponse::Slots::Count),
Status,
StatusMessage,
Redirected,
Count,
};
static const JSFunctionSpec static_methods[];
Expand All @@ -204,6 +205,7 @@ class Response final : public BuiltinImpl<Response> {
static bool init_class(JSContext *cx, JS::HandleObject global);
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);

static bool redirect(JSContext *cx, unsigned argc, JS::Value *vp);
static JSObject *create(JSContext *cx, JS::HandleObject response,
fastly_response_handle_t response_handle,
fastly_body_handle_t body_handle, bool is_upstream);
Expand Down
2 changes: 2 additions & 0 deletions c-dependencies/js-compute-runtime/error-numbers.msg
Expand Up @@ -103,4 +103,6 @@ MSG_DEF(JSMSG_REQUEST_BACKEND_DOES_NOT_EXIST, 1, JSEXN_TYPEERR,
MSG_DEF(JSMSG_SUBTLE_CRYPTO_ERROR, 1, JSEXN_ERR, "{0}")
MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_JWK_KTY_VALUE, 1, JSEXN_ERR, "The JWK 'kty' member was not '{0}'")
MSG_DEF(JSMSG_SUBTLE_CRYPTO_INVALID_KEY_USAGES_VALUE, 0, JSEXN_TYPEERR, "Invalid keyUsages argument")
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_URI, 0, JSEXN_TYPEERR, "Response.redirect: url parameter is not a valid URL.")
MSG_DEF(JSMSG_RESPONSE_REDIRECT_INVALID_STATUS, 0, JSEXN_RANGEERR, "Response.redirect: Invalid redirect status code.")
//clang-format on
@@ -0,0 +1,44 @@
/* eslint-env serviceworker */
import { pass, assert, assertThrows } from "../../../assertions.js";
import { routes } from "./test-harness.js";

routes.set("/response/redirect", async () => {
const url = "http://test.url:1234/";
const redirectResponse = Response.redirect(url);
let error = assert(redirectResponse.type, "default");
if (error) { return error; }
error = assert(redirectResponse.redirected, false);
if (error) { return error; }
error = assert(redirectResponse.ok, false);
if (error) { return error; }
error = assert(redirectResponse.status, 302, "Default redirect status is 302");
if (error) { return error; }
error = assert(redirectResponse.headers.get("Location"), url)
if (error) { return error; }
error = assert(redirectResponse.statusText, "");
if (error) { return error; }

for (const status of [301, 302, 303, 307, 308]) {
const redirectResponse = Response.redirect(url, status);
error = assert(redirectResponse.type, "default");
if (error) { return error; }
error = assert(redirectResponse.redirected, false);
if (error) { return error; }
error = assert(redirectResponse.ok, false);
if (error) { return error; }
error = assert(redirectResponse.status, status, "Redirect status is " + status);
if (error) { return error; }
error = assert(redirectResponse.headers.get("Location"), url);
if (error) { return error; }
error = assert(redirectResponse.statusText, "");
if (error) { return error; }
}
const invalidUrl = "http://:This is not an url";
error = assertThrows(function () { Response.redirect(invalidUrl); }, TypeError);
if (error) { return error; }
for (const invalidStatus of [200, 309, 400, 500]) {
error = assertThrows(function () { Response.redirect(url, invalidStatus); }, RangeError);
if (error) { return error; }
}
return pass()
})
@@ -0,0 +1,32 @@
/* eslint-env serviceworker */
import { env } from 'fastly:env';
import { fail } from "../../../assertions.js";

addEventListener("fetch", event => {
event.respondWith(app(event))
})
/**
* @param {FetchEvent} event
* @returns {Response}
*/
async function app(event) {
try {
const path = (new URL(event.request.url)).pathname
console.log(`path: ${path}`)
console.log(`FASTLY_SERVICE_VERSION: ${env('FASTLY_SERVICE_VERSION')}`)
if (routes.has(path)) {
const routeHandler = routes.get(path)
return await routeHandler()
}
return fail(`${path} endpoint does not exist`)
} catch (error) {
return fail(`The routeHandler threw an error: ${error.message}` + '\n' + error.stack)
}
}

export const routes = new Map()
routes.set('/', () => {
routes.delete('/')
let test_routes = Array.from(routes.keys())
return new Response(JSON.stringify(test_routes), { 'headers': { 'content-type': 'application/json' } })
})
@@ -0,0 +1,12 @@
# This file describes a Fastly Compute@Edge package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["jchampion@fastly.com"]
description = ""
language = "other"
manifest_version = 2
name = "response-redirect"
service_id = ""

[scripts]
build = "node ../../../../js-compute-runtime-cli.js"
12 changes: 12 additions & 0 deletions integration-tests/js-compute/fixtures/response-redirect/tests.json
@@ -0,0 +1,12 @@
{
"GET /response/redirect": {
"environments": ["viceroy", "c@e"],
"downstream_request": {
"method": "GET",
"pathname": "/response/redirect"
},
"downstream_response": {
"status": 200
}
}
}
@@ -1,35 +1,35 @@
{
"Check default redirect response": {
"status": "FAIL"
"status": "PASS"
},
"Check response returned by static method redirect(), status = 301": {
"status": "FAIL"
"status": "PASS"
},
"Check response returned by static method redirect(), status = 302": {
"status": "FAIL"
"status": "PASS"
},
"Check response returned by static method redirect(), status = 303": {
"status": "FAIL"
"status": "PASS"
},
"Check response returned by static method redirect(), status = 307": {
"status": "FAIL"
"status": "PASS"
},
"Check response returned by static method redirect(), status = 308": {
"status": "FAIL"
"status": "PASS"
},
"Check error returned when giving invalid url to redirect()": {
"status": "PASS"
},
"Check error returned when giving invalid status to redirect(), status = 200": {
"status": "FAIL"
"status": "PASS"
},
"Check error returned when giving invalid status to redirect(), status = 309": {
"status": "FAIL"
"status": "PASS"
},
"Check error returned when giving invalid status to redirect(), status = 400": {
"status": "FAIL"
"status": "PASS"
},
"Check error returned when giving invalid status to redirect(), status = 500": {
"status": "FAIL"
"status": "PASS"
}
}

0 comments on commit 1623d74

Please sign in to comment.