Skip to content

Commit

Permalink
avoid reusing ReadableStream (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
dferber90 committed Mar 17, 2023
1 parent 6e85616 commit 2df8aac
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-oranges-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/edge-config': patch
---

avoid reusing ReadableStream across request contexts
10 changes: 6 additions & 4 deletions src/index.edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export function createClient(
// the edge config itself does not exist
throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND);
}
if (res.cachedResponse) return res.cachedResponse.json();
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as T;
if (res.ok) return res.json();
throw new Error(ERRORS.UNEXPECTED);
},
Expand Down Expand Up @@ -109,7 +110,8 @@ export function createClient(
// the /items endpoint never returns 404, so if we get a 404
// it means the edge config itself did not exist
if (res.status === 404) throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND);
if (res.cachedResponse) return res.cachedResponse.json();
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as T;
if (res.ok) return res.json();
throw new Error(ERRORS.UNEXPECTED);
},
Expand All @@ -123,8 +125,8 @@ export function createClient(
headers: new Headers(headers),
}).then(
(res) => {
if (res.cachedResponse)
return res.cachedResponse.json() as Promise<string>;
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as string;
if (res.ok) return res.json() as Promise<string>;
throw new Error(ERRORS.UNEXPECTED);
},
Expand Down
10 changes: 6 additions & 4 deletions src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export function createClient(
// the edge config itself does not exist
throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND);
}
if (res.cachedResponse) return res.cachedResponse.json();
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as T;
if (res.ok) return res.json();
throw new Error(ERRORS.UNEXPECTED);
},
Expand Down Expand Up @@ -163,7 +164,8 @@ export function createClient(
// the /items endpoint never returns 404, so if we get a 404
// it means the edge config itself did not exist
if (res.status === 404) throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND);
if (res.cachedResponse) return res.cachedResponse.json();
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as T;
if (res.ok) return res.json();
throw new Error(ERRORS.UNEXPECTED);
},
Expand All @@ -183,8 +185,8 @@ export function createClient(
headers: new Headers(headers),
}).then(
(res) => {
if (res.cachedResponse)
return res.cachedResponse.json() as Promise<string>;
if (res.cachedResponseBody !== undefined)
return res.cachedResponseBody as string;
if (res.ok) return res.json() as Promise<string>;
throw new Error(ERRORS.UNEXPECTED);
},
Expand Down
14 changes: 4 additions & 10 deletions src/utils/fetch-with-cached-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('fetchWithCachedResponse', () => {
new Headers({ ETag: 'abc123', 'content-type': 'application/json' }),
);
await expect(data1.json()).resolves.toEqual({ name: 'John' });
expect(data1.cachedResponse).toBeUndefined();
expect(data1.cachedResponseBody).toBeUndefined();

// Second request (should come from cache)
fetchMock.mockResponseOnce('', {
Expand All @@ -45,10 +45,7 @@ describe('fetchWithCachedResponse', () => {
);

expect(data2).toHaveProperty('status', 304);
expect(data2.cachedResponse).toHaveProperty('status', 200);
await expect(data2.cachedResponse?.json()).resolves.toEqual({
name: 'John',
});
expect(data2.cachedResponseBody).toEqual({ name: 'John' });
});

it('should differentiate caches by authorization header', async () => {
Expand Down Expand Up @@ -93,7 +90,7 @@ describe('fetchWithCachedResponse', () => {
new Headers({ ETag: 'abc123', 'content-type': 'application/json' }),
);
expect(data2).toHaveProperty('status', 200);
expect(data2.cachedResponse).toBeUndefined();
expect(data2.cachedResponseBody).toBeUndefined();
await expect(data2.json()).resolves.toEqual({
name: 'Bob',
});
Expand Down Expand Up @@ -122,9 +119,6 @@ describe('fetchWithCachedResponse', () => {
);

expect(data3).toHaveProperty('status', 304);
expect(data3.cachedResponse).toHaveProperty('status', 200);
await expect(data3.cachedResponse?.json()).resolves.toEqual({
name: 'John',
});
expect(data3.cachedResponseBody).toEqual({ name: 'John' });
});
});
15 changes: 10 additions & 5 deletions src/utils/fetch-with-cached-response.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export interface CachedResponsePair {
etag: string;
response: Response;
response: string;
}

type FetchOptions = Omit<RequestInit, 'headers'> & { headers?: Headers };

interface ResponseWithCachedResponse extends Response {
cachedResponse?: Response;
cachedResponseBody?: unknown;
}

export const cache = new Map<string, CachedResponsePair>();
Expand All @@ -32,18 +32,23 @@ export async function fetchWithCachedResponse(
});

if (res.status === 304) {
res.cachedResponse = cachedResponse.clone();
res.cachedResponseBody = JSON.parse(cachedResponse);
return res;
}

const newETag = res.headers.get('ETag');
if (res.ok && newETag)
cache.set(cacheKey, { etag: newETag, response: res.clone() });
cache.set(cacheKey, {
etag: newETag,
response: await res.clone().text(),
});
return res;
}

const res = await fetch(url, options);
const etag = res.headers.get('ETag');
if (res.ok && etag) cache.set(cacheKey, { etag, response: res.clone() });
if (res.ok && etag)
cache.set(cacheKey, { etag, response: await res.clone().text() });

return res;
}

0 comments on commit 2df8aac

Please sign in to comment.