Skip to content

Commit e08d060

Browse files
authored
feat: add ability to automatically decompress gzip responses returned from fetch (#497)
1 parent 9043103 commit e08d060

File tree

13 files changed

+255
-2
lines changed

13 files changed

+255
-2
lines changed

documentation/docs/globals/Request/Request.mdx

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ new Request(input, options)
4040
- : Any body that you want to add to your request: this can be an `ArrayBuffer`, a `TypedArray`, a `DataView`, a `URLSearchParams`, string object or literal, or a `ReadableStream` object.
4141
- `backend` _**Fastly-specific**_
4242
- `cacheOverride` _**Fastly-specific**_
43-
- `cacheKey` _**Fastly-specific**_
43+
- `cacheKey` _**Fastly-specific**_
44+
- `fastly` _**Fastly-specific**_
45+
- `decompressGzip`_: boolean_ _**optional**_
46+
- Whether to automatically gzip decompress the Response or not.

documentation/docs/globals/fetch.mdx

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ fetch(resource, options)
5656
- *Fastly-specific*
5757
- `cacheOverride` _**Fastly-specific**_
5858
- `cacheKey` _**Fastly-specific**_
59+
- `fastly` _**Fastly-specific**_
60+
- `decompressGzip`_: boolean_ _**optional**_
61+
- Whether to automatically gzip decompress the Response or not.
5962

6063
### Return value
6164

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* eslint-env serviceworker */
2+
import { env } from 'fastly:env';
3+
import { pass, fail, assert, assertRejects } 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+
34+
// Request.fastly.decompressGzip option -- automatic gzip decompression of responses
35+
{
36+
routes.set("/request/constructor/fastly/decompressGzip/true", async () => {
37+
const request = new Request('https://httpbin.org/gzip', {
38+
headers: {
39+
accept: 'application/json'
40+
},
41+
backend: "httpbin",
42+
fastly: {
43+
decompressGzip: true
44+
}
45+
});
46+
const response = await fetch(request);
47+
// This should work because the response will be decompressed and valid json.
48+
const body = await response.json();
49+
let error = assert(body.gzipped, true, `body.gzipped`)
50+
if (error) { return error }
51+
return pass()
52+
});
53+
routes.set("/request/constructor/fastly/decompressGzip/false", async () => {
54+
const request = new Request('https://httpbin.org/gzip', {
55+
headers: {
56+
accept: 'application/json'
57+
},
58+
backend: "httpbin",
59+
fastly: {
60+
decompressGzip: false
61+
}
62+
});
63+
const response = await fetch(request);
64+
let error = await assertRejects(async function() {
65+
// This should throw because the response will be gzipped compressed, which we can not parse as json.
66+
await response.json();
67+
});
68+
if (error) { return error }
69+
return pass()
70+
});
71+
72+
routes.set("/fetch/requestinit/fastly/decompressGzip/true", async () => {
73+
const response = await fetch('https://httpbin.org/gzip', {
74+
headers: {
75+
accept: 'application/json'
76+
},
77+
backend: "httpbin",
78+
fastly: {
79+
decompressGzip: true
80+
}
81+
});
82+
// This should work because the response will be decompressed and valid json.
83+
const body = await response.json();
84+
let error = assert(body.gzipped, true, `body.gzipped`)
85+
if (error) { return error }
86+
return pass()
87+
});
88+
routes.set("/fetch/requestinit/fastly/decompressGzip/false", async () => {
89+
const response = await fetch('https://httpbin.org/gzip', {
90+
headers: {
91+
accept: 'application/json'
92+
},
93+
backend: "httpbin",
94+
fastly: {
95+
decompressGzip: false
96+
}
97+
});
98+
let error = await assertRejects(async function() {
99+
// This should throw because the response will be gzipped compressed, which we can not parse as json.
100+
await response.json();
101+
});
102+
if (error) { return error }
103+
return pass()
104+
});
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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-auto-decompress"
9+
service_id = ""
10+
11+
[scripts]
12+
build = "node ../../../../js-compute-runtime-cli.js"
13+
14+
[local_server]
15+
[local_server.backends]
16+
[local_server.backends.httpbin]
17+
url = "https://httpbin.org/"
18+
19+
[setup]
20+
[setup.backends]
21+
[setup.backends.httpbin]
22+
address = "httpbin.org"
23+
port = 443
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"GET /request/constructor/fastly/decompressGzip/true": {
3+
"environments": ["viceroy", "c@e"],
4+
"downstream_request": {
5+
"method": "GET",
6+
"pathname": "/request/constructor/fastly/decompressGzip/true"
7+
},
8+
"downstream_response": {
9+
"status": 200
10+
}
11+
},
12+
"GET /request/constructor/fastly/decompressGzip/false": {
13+
"environments": ["viceroy", "c@e"],
14+
"downstream_request": {
15+
"method": "GET",
16+
"pathname": "/request/constructor/fastly/decompressGzip/false"
17+
},
18+
"downstream_response": {
19+
"status": 200
20+
}
21+
},
22+
"GET /fetch/requestinit/fastly/decompressGzip/true": {
23+
"environments": ["viceroy", "c@e"],
24+
"downstream_request": {
25+
"method": "GET",
26+
"pathname": "/fetch/requestinit/fastly/decompressGzip/true"
27+
},
28+
"downstream_response": {
29+
"status": 200
30+
}
31+
},
32+
"GET /fetch/requestinit/fastly/decompressGzip/false": {
33+
"environments": ["viceroy", "c@e"],
34+
"downstream_request": {
35+
"method": "GET",
36+
"pathname": "/fetch/requestinit/fastly/decompressGzip/false"
37+
},
38+
"downstream_response": {
39+
"status": 200
40+
}
41+
}
42+
}

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

+40-1
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,25 @@ bool Request::set_cache_override(JSContext *cx, JS::HandleObject self,
11431143
return true;
11441144
}
11451145

1146+
bool Request::apply_auto_decompress_gzip(JSContext *cx, JS::HandleObject self) {
1147+
MOZ_ASSERT(cx);
1148+
MOZ_ASSERT(is_instance(self));
1149+
auto decompress =
1150+
JS::GetReservedSlot(self, static_cast<uint32_t>(Request::Slots::AutoDecompressGzip))
1151+
.toBoolean();
1152+
if (!decompress) {
1153+
return true;
1154+
}
1155+
1156+
auto res = Request::request_handle(self).auto_decompress_gzip();
1157+
if (auto *err = res.to_err()) {
1158+
HANDLE_ERROR(cx, *err);
1159+
return false;
1160+
}
1161+
1162+
return true;
1163+
}
1164+
11461165
/**
11471166
* Apply the CacheOverride to a host-side request handle.
11481167
*/
@@ -1632,13 +1651,15 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H
16321651
JS::RootedValue body_val(cx);
16331652
JS::RootedValue backend_val(cx);
16341653
JS::RootedValue cache_override(cx);
1654+
JS::RootedValue fastly_val(cx);
16351655
if (init_val.isObject()) {
16361656
JS::RootedObject init(cx, init_val.toObjectOrNull());
16371657
if (!JS_GetProperty(cx, init, "method", &method_val) ||
16381658
!JS_GetProperty(cx, init, "headers", &headers_val) ||
16391659
!JS_GetProperty(cx, init, "body", &body_val) ||
16401660
!JS_GetProperty(cx, init, "backend", &backend_val) ||
1641-
!JS_GetProperty(cx, init, "cacheOverride", &cache_override)) {
1661+
!JS_GetProperty(cx, init, "cacheOverride", &cache_override) ||
1662+
!JS_GetProperty(cx, init, "fastly", &fastly_val)) {
16421663
return nullptr;
16431664
}
16441665
} else if (!init_val.isNullOrUndefined()) {
@@ -1931,6 +1952,24 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H
19311952
JS::GetReservedSlot(input_request, static_cast<uint32_t>(Slots::CacheOverride)));
19321953
}
19331954

1955+
if (fastly_val.isObject()) {
1956+
JS::RootedValue decompress_response_val(cx);
1957+
JS::RootedObject fastly(cx, fastly_val.toObjectOrNull());
1958+
if (!JS_GetProperty(cx, fastly, "decompressGzip", &decompress_response_val)) {
1959+
return nullptr;
1960+
}
1961+
auto value = JS::ToBoolean(decompress_response_val);
1962+
JS::SetReservedSlot(request, static_cast<uint32_t>(Slots::AutoDecompressGzip),
1963+
JS::BooleanValue(value));
1964+
} else if (input_request) {
1965+
JS::SetReservedSlot(
1966+
request, static_cast<uint32_t>(Slots::AutoDecompressGzip),
1967+
JS::GetReservedSlot(input_request, static_cast<uint32_t>(Slots::AutoDecompressGzip)));
1968+
} else {
1969+
JS::SetReservedSlot(request, static_cast<uint32_t>(Slots::AutoDecompressGzip),
1970+
JS::BooleanValue(false));
1971+
}
1972+
19341973
return request;
19351974
}
19361975

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

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class Request final : public BuiltinImpl<Request> {
130130
PendingRequest,
131131
ResponsePromise,
132132
IsDownstream,
133+
AutoDecompressGzip,
133134
Count,
134135
};
135136

@@ -139,6 +140,7 @@ class Request final : public BuiltinImpl<Request> {
139140
static bool set_cache_override(JSContext *cx, JS::HandleObject self,
140141
JS::HandleValue cache_override_val);
141142
static bool apply_cache_override(JSContext *cx, JS::HandleObject self);
143+
static bool apply_auto_decompress_gzip(JSContext *cx, JS::HandleObject self);
142144

143145
static host_api::HttpReq request_handle(JSObject *obj);
144146
static host_api::HttpPendingReq pending_handle(JSObject *obj);

runtime/js-compute-runtime/host_interface/component/fastly_world_adapter.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ bool fastly_compute_at_edge_http_req_cache_override_set(
191191
err);
192192
}
193193

194+
bool fastly_compute_at_edge_http_req_auto_decompress_response_set(
195+
fastly_compute_at_edge_http_types_request_handle_t h,
196+
fastly_compute_at_edge_http_types_content_encodings_t encodings,
197+
fastly_compute_at_edge_types_error_t *err) {
198+
return convert_result(fastly::req_auto_decompress_response_set(h, encodings), err);
199+
}
200+
194201
bool fastly_compute_at_edge_http_req_downstream_client_ip_addr(
195202
fastly_world_list_u8_t *ret, fastly_compute_at_edge_types_error_t *err) {
196203
ret->ptr = static_cast<uint8_t *>(cabi_malloc(16, 1));

runtime/js-compute-runtime/host_interface/fastly.h

+4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ int req_cache_override_v2_set(fastly_compute_at_edge_http_types_request_handle_t
145145
int tag, uint32_t ttl, uint32_t stale_while_revalidate,
146146
const char *surrogate_key, size_t surrogate_key_len);
147147

148+
WASM_IMPORT("fastly_http_req", "auto_decompress_response_set")
149+
int req_auto_decompress_response_set(fastly_compute_at_edge_http_types_request_handle_t req_handle,
150+
int tag);
151+
148152
/**
149153
* `octets` must be a 16-byte array.
150154
* If, after a successful call, `nwritten` == 4, the value in `octets` is an IPv4 address.

runtime/js-compute-runtime/host_interface/host_api.cpp

+16
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,22 @@ Result<Void> HttpReq::redirect_to_grip_proxy(std::string_view backend) {
381381
return res;
382382
}
383383

384+
Result<Void> HttpReq::auto_decompress_gzip() {
385+
Result<Void> res;
386+
387+
fastly_compute_at_edge_types_error_t err;
388+
fastly_compute_at_edge_http_types_content_encodings_t encodings_to_decompress = 0;
389+
encodings_to_decompress |= FASTLY_COMPUTE_AT_EDGE_HTTP_TYPES_CONTENT_ENCODINGS_GZIP;
390+
if (!fastly_compute_at_edge_http_req_auto_decompress_response_set(
391+
this->handle, encodings_to_decompress, &err)) {
392+
res.emplace_err(err);
393+
} else {
394+
res.emplace();
395+
}
396+
397+
return res;
398+
}
399+
384400
Result<Void> HttpReq::register_dynamic_backend(std::string_view name, std::string_view target,
385401
const BackendConfig &config) {
386402
Result<Void> res;

runtime/js-compute-runtime/host_interface/host_api.h

+2
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ class HttpReq final : public HttpBase {
366366

367367
static Result<HostBytes> http_req_downstream_tls_ja3_md5();
368368

369+
Result<Void> auto_decompress_gzip();
370+
369371
/// Send this request synchronously, and wait for the response.
370372
Result<Response> send(HttpBody body, std::string_view backend);
371373

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

+4
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,10 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) {
680680
return false;
681681
}
682682

683+
if (!builtins::Request::apply_auto_decompress_gzip(cx, request)) {
684+
return false;
685+
}
686+
683687
bool streaming = false;
684688
if (!builtins::RequestOrResponse::maybe_stream_body(cx, request, &streaming)) {
685689
return false;

types/globals.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,9 @@ declare interface RequestInit {
10331033
backend?: string;
10341034
cacheOverride?: import('fastly:cache-override').CacheOverride;
10351035
cacheKey?: string;
1036+
fastly?: {
1037+
decompressGzip?: boolean
1038+
}
10361039
}
10371040

10381041
/**

0 commit comments

Comments
 (0)