Skip to content

Commit 95d1c45

Browse files
committed
feat: add decompress option support with Fastly decompressGzip mapping (#81)
Implement cross-platform decompression control by mapping the @adobe/fetch decompress option to platform-specific behavior: - Fastly: Maps decompress to fastly.decompressGzip - Cloudflare: Pass-through (auto-decompresses) - Node.js: Pass-through to @adobe/fetch The wrapper accepts decompress: true|false (default: true) and automatically sets fastly.decompressGzip when running on Fastly Compute. Explicit fastly options take precedence over the mapped value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Lars Trieloff <[email protected]>
1 parent c51845c commit 95d1c45

File tree

2 files changed

+226
-6
lines changed

2 files changed

+226
-6
lines changed

src/template/polyfills/fetch.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,58 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
/* eslint-env serviceworker */
13+
/* global Dictionary */
1314

14-
module.exports = {
15-
// replacing @adobe/fetch with the built-in APIs
16-
fetch,
17-
Request,
18-
Response,
19-
Headers,
15+
/**
16+
* Detects if the code is running in a Fastly Compute environment.
17+
* @returns {boolean} true if running on Fastly
18+
*/
19+
function isFastlyEnvironment() {
20+
try {
21+
// Dictionary is a Fastly-specific global used for config/secrets
22+
return typeof Dictionary !== 'undefined';
23+
} catch {
24+
return false;
25+
}
26+
}
27+
28+
/**
29+
* Wrapper for fetch that provides cross-platform decompression support.
30+
* Maps the @adobe/fetch `decompress` option to platform-specific behavior:
31+
* - Fastly: Sets fastly.decompressGzip based on decompress value
32+
* - Cloudflare: No-op (automatically decompresses)
33+
* - Node.js: Pass through to @adobe/fetch (handles it natively)
34+
*
35+
* @param {RequestInfo} resource - URL or Request object
36+
* @param {RequestInit & {decompress?: boolean, fastly?: object}} options - Fetch options
37+
* @returns {Promise<Response>} The fetch response
38+
*/
39+
function wrappedFetch(resource, options = {}) {
40+
// Extract decompress option (default: true to match @adobe/fetch behavior)
41+
const { decompress = true, fastly, ...otherOptions } = options;
42+
43+
// On Fastly: map decompress to fastly.decompressGzip
44+
if (isFastlyEnvironment()) {
45+
const fastlyOptions = {
46+
decompressGzip: decompress,
47+
...fastly, // explicit fastly options override
48+
};
49+
return fetch(resource, { ...otherOptions, fastly: fastlyOptions });
50+
}
51+
52+
// On Cloudflare/Node.js: pass through as-is
53+
// Cloudflare auto-decompresses, Node.js uses @adobe/fetch which handles decompress
54+
return fetch(resource, options);
55+
}
56+
57+
// Export wrapped fetch and native Web APIs
58+
export { wrappedFetch as fetch };
59+
export const { Request, Response, Headers } = globalThis;
60+
61+
// Export for CommonJS (for compatibility with require() in bundled code)
62+
export default {
63+
fetch: wrappedFetch,
64+
Request: globalThis.Request,
65+
Response: globalThis.Response,
66+
Headers: globalThis.Headers,
2067
};

test/fetch-polyfill.test.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/* eslint-env mocha */
14+
15+
import assert from 'assert';
16+
17+
describe('Fetch Polyfill Test', () => {
18+
let fetchPolyfill;
19+
let originalFetch;
20+
let fetchCalls;
21+
22+
before(async () => {
23+
// Import the module once
24+
fetchPolyfill = await import('../src/template/polyfills/fetch.js');
25+
});
26+
27+
beforeEach(() => {
28+
// Save original fetch
29+
originalFetch = global.fetch;
30+
31+
// Mock fetch to capture calls
32+
fetchCalls = [];
33+
global.fetch = (resource, options) => {
34+
fetchCalls.push({ resource, options });
35+
return Promise.resolve(new Response('mocked'));
36+
};
37+
});
38+
39+
afterEach(() => {
40+
// Restore original fetch
41+
global.fetch = originalFetch;
42+
// Clean up Dictionary if set
43+
delete global.Dictionary;
44+
});
45+
46+
describe('Fastly environment', () => {
47+
beforeEach(() => {
48+
global.Dictionary = true;
49+
});
50+
51+
it('maps decompress: true to fastly.decompressGzip: true by default', async () => {
52+
await fetchPolyfill.fetch('https://example.com');
53+
54+
assert.strictEqual(fetchCalls.length, 1);
55+
assert.strictEqual(fetchCalls[0].resource, 'https://example.com');
56+
assert.deepStrictEqual(fetchCalls[0].options, {
57+
fastly: { decompressGzip: true },
58+
});
59+
});
60+
61+
it('maps decompress: true to fastly.decompressGzip: true explicitly', async () => {
62+
await fetchPolyfill.fetch('https://example.com', { decompress: true });
63+
64+
assert.strictEqual(fetchCalls.length, 1);
65+
assert.deepStrictEqual(fetchCalls[0].options, {
66+
fastly: { decompressGzip: true },
67+
});
68+
});
69+
70+
it('maps decompress: false to fastly.decompressGzip: false', async () => {
71+
await fetchPolyfill.fetch('https://example.com', { decompress: false });
72+
73+
assert.strictEqual(fetchCalls.length, 1);
74+
assert.deepStrictEqual(fetchCalls[0].options, {
75+
fastly: { decompressGzip: false },
76+
});
77+
});
78+
79+
it('passes through explicit fastly options', async () => {
80+
await fetchPolyfill.fetch('https://example.com', {
81+
fastly: { decompressGzip: false, backend: 'custom' },
82+
});
83+
84+
assert.strictEqual(fetchCalls.length, 1);
85+
assert.deepStrictEqual(fetchCalls[0].options, {
86+
fastly: { decompressGzip: false, backend: 'custom' },
87+
});
88+
});
89+
90+
it('explicit fastly options override decompress mapping', async () => {
91+
await fetchPolyfill.fetch('https://example.com', {
92+
decompress: true,
93+
fastly: { decompressGzip: false },
94+
});
95+
96+
assert.strictEqual(fetchCalls.length, 1);
97+
assert.deepStrictEqual(fetchCalls[0].options, {
98+
fastly: { decompressGzip: false },
99+
});
100+
});
101+
102+
it('preserves other fetch options', async () => {
103+
await fetchPolyfill.fetch('https://example.com', {
104+
method: 'POST',
105+
headers: { 'Content-Type': 'application/json' },
106+
decompress: true,
107+
});
108+
109+
assert.strictEqual(fetchCalls.length, 1);
110+
assert.strictEqual(fetchCalls[0].options.method, 'POST');
111+
assert.deepStrictEqual(fetchCalls[0].options.headers, {
112+
'Content-Type': 'application/json',
113+
});
114+
assert.deepStrictEqual(fetchCalls[0].options.fastly, {
115+
decompressGzip: true,
116+
});
117+
});
118+
119+
it('merges fastly options with decompress mapping', async () => {
120+
await fetchPolyfill.fetch('https://example.com', {
121+
decompress: true,
122+
fastly: { backend: 'custom-backend' },
123+
});
124+
125+
assert.strictEqual(fetchCalls.length, 1);
126+
assert.deepStrictEqual(fetchCalls[0].options, {
127+
fastly: {
128+
decompressGzip: true,
129+
backend: 'custom-backend',
130+
},
131+
});
132+
});
133+
});
134+
135+
describe('Non-Fastly environment (Cloudflare/Node.js)', () => {
136+
it('passes through options as-is when decompress is provided', async () => {
137+
await fetchPolyfill.fetch('https://example.com', {
138+
decompress: true,
139+
method: 'GET',
140+
});
141+
142+
assert.strictEqual(fetchCalls.length, 1);
143+
assert.deepStrictEqual(fetchCalls[0].options, {
144+
decompress: true,
145+
method: 'GET',
146+
});
147+
});
148+
149+
it('passes through options as-is when no decompress is provided', async () => {
150+
await fetchPolyfill.fetch('https://example.com', {
151+
method: 'POST',
152+
headers: { 'Content-Type': 'text/plain' },
153+
});
154+
155+
assert.strictEqual(fetchCalls.length, 1);
156+
assert.deepStrictEqual(fetchCalls[0].options, {
157+
method: 'POST',
158+
headers: { 'Content-Type': 'text/plain' },
159+
});
160+
});
161+
162+
it('preserves fastly options in non-Fastly environment', async () => {
163+
await fetchPolyfill.fetch('https://example.com', {
164+
fastly: { backend: 'test' },
165+
});
166+
167+
assert.strictEqual(fetchCalls.length, 1);
168+
assert.deepStrictEqual(fetchCalls[0].options, {
169+
fastly: { backend: 'test' },
170+
});
171+
});
172+
});
173+
});

0 commit comments

Comments
 (0)