Skip to content

Commit 7c38f14

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 <noreply@anthropic.com> Signed-off-by: Lars Trieloff <lars@trieloff.net>
1 parent c51845c commit 7c38f14

File tree

2 files changed

+228
-6
lines changed

2 files changed

+228
-6
lines changed

src/template/polyfills/fetch.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,56 @@
1111
*/
1212
/* eslint-env serviceworker */
1313

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

test/fetch-polyfill.test.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 originalCaches;
21+
let fetchCalls;
22+
23+
before(async () => {
24+
// Import the module once
25+
fetchPolyfill = await import('../src/template/polyfills/fetch.js');
26+
});
27+
28+
beforeEach(() => {
29+
// Save original fetch and caches
30+
originalFetch = global.fetch;
31+
originalCaches = global.caches;
32+
33+
// Mock fetch to capture calls
34+
fetchCalls = [];
35+
global.fetch = (resource, options) => {
36+
fetchCalls.push({ resource, options });
37+
return Promise.resolve(new Response('mocked'));
38+
};
39+
});
40+
41+
afterEach(() => {
42+
// Restore original fetch and caches
43+
global.fetch = originalFetch;
44+
global.caches = originalCaches;
45+
});
46+
47+
describe('Cloudflare environment', () => {
48+
beforeEach(() => {
49+
// Mock Cloudflare's caches global
50+
global.caches = { default: {} };
51+
});
52+
53+
it('passes through options as-is with decompress: true', async () => {
54+
await fetchPolyfill.fetch('https://example.com', { decompress: true });
55+
56+
assert.strictEqual(fetchCalls.length, 1);
57+
assert.deepStrictEqual(fetchCalls[0].options, {
58+
decompress: true,
59+
});
60+
});
61+
62+
it('passes through options as-is with decompress: false', async () => {
63+
await fetchPolyfill.fetch('https://example.com', { decompress: false });
64+
65+
assert.strictEqual(fetchCalls.length, 1);
66+
assert.deepStrictEqual(fetchCalls[0].options, {
67+
decompress: false,
68+
});
69+
});
70+
71+
it('passes through fastly options without modification', async () => {
72+
await fetchPolyfill.fetch('https://example.com', {
73+
fastly: { backend: 'custom' },
74+
});
75+
76+
assert.strictEqual(fetchCalls.length, 1);
77+
assert.deepStrictEqual(fetchCalls[0].options, {
78+
fastly: { backend: 'custom' },
79+
});
80+
});
81+
82+
it('preserves all options unchanged', async () => {
83+
await fetchPolyfill.fetch('https://example.com', {
84+
method: 'POST',
85+
headers: { 'Content-Type': 'application/json' },
86+
decompress: true,
87+
});
88+
89+
assert.strictEqual(fetchCalls.length, 1);
90+
assert.deepStrictEqual(fetchCalls[0].options, {
91+
method: 'POST',
92+
headers: { 'Content-Type': 'application/json' },
93+
decompress: true,
94+
});
95+
});
96+
});
97+
98+
describe('Non-Cloudflare environment (Fastly/Node.js)', () => {
99+
beforeEach(() => {
100+
// Ensure no Cloudflare caches global
101+
delete global.caches;
102+
});
103+
104+
it('maps decompress: true to fastly.decompressGzip: true by default', async () => {
105+
await fetchPolyfill.fetch('https://example.com');
106+
107+
assert.strictEqual(fetchCalls.length, 1);
108+
assert.strictEqual(fetchCalls[0].resource, 'https://example.com');
109+
assert.deepStrictEqual(fetchCalls[0].options, {
110+
fastly: { decompressGzip: true },
111+
});
112+
});
113+
114+
it('maps decompress: true to fastly.decompressGzip: true explicitly', async () => {
115+
await fetchPolyfill.fetch('https://example.com', { decompress: true });
116+
117+
assert.strictEqual(fetchCalls.length, 1);
118+
assert.deepStrictEqual(fetchCalls[0].options, {
119+
fastly: { decompressGzip: true },
120+
});
121+
});
122+
123+
it('maps decompress: false to fastly.decompressGzip: false', async () => {
124+
await fetchPolyfill.fetch('https://example.com', { decompress: false });
125+
126+
assert.strictEqual(fetchCalls.length, 1);
127+
assert.deepStrictEqual(fetchCalls[0].options, {
128+
fastly: { decompressGzip: false },
129+
});
130+
});
131+
132+
it('explicit fastly options override decompress mapping', async () => {
133+
await fetchPolyfill.fetch('https://example.com', {
134+
decompress: true,
135+
fastly: { decompressGzip: false },
136+
});
137+
138+
assert.strictEqual(fetchCalls.length, 1);
139+
assert.deepStrictEqual(fetchCalls[0].options, {
140+
fastly: { decompressGzip: false },
141+
});
142+
});
143+
144+
it('preserves other fetch options', async () => {
145+
await fetchPolyfill.fetch('https://example.com', {
146+
method: 'POST',
147+
headers: { 'Content-Type': 'application/json' },
148+
decompress: true,
149+
});
150+
151+
assert.strictEqual(fetchCalls.length, 1);
152+
assert.strictEqual(fetchCalls[0].options.method, 'POST');
153+
assert.deepStrictEqual(fetchCalls[0].options.headers, {
154+
'Content-Type': 'application/json',
155+
});
156+
assert.deepStrictEqual(fetchCalls[0].options.fastly, {
157+
decompressGzip: true,
158+
});
159+
});
160+
161+
it('merges fastly options with decompress mapping', async () => {
162+
await fetchPolyfill.fetch('https://example.com', {
163+
decompress: true,
164+
fastly: { backend: 'custom-backend' },
165+
});
166+
167+
assert.strictEqual(fetchCalls.length, 1);
168+
assert.deepStrictEqual(fetchCalls[0].options, {
169+
fastly: {
170+
decompressGzip: true,
171+
backend: 'custom-backend',
172+
},
173+
});
174+
});
175+
});
176+
});

0 commit comments

Comments
 (0)