Skip to content

Commit 99d8909

Browse files
conico974Nicolas Dorseuil
andauthored
Add binary content type handling in cache interceptor (#954)
* feat: Add binary content type handling in cacheInterceptor * add test for route type * changeset * linting --------- Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent 0d57c11 commit 99d8909

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

.changeset/perfect-jeans-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
add support for route type in cache interceptor

packages/open-next/src/core/routing/cacheInterceptor.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { InternalEvent, InternalResult } from "types/open-next";
55
import type { CacheValue } from "types/overrides";
66
import { emptyReadableStream, toReadableStream } from "utils/stream";
77

8+
import { isBinaryContentType } from "utils/binary";
89
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
910
import { debug } from "../../adapters/logger";
1011
import { localizePath } from "./i18n";
@@ -268,6 +269,31 @@ export async function cacheInterceptor(
268269
isBase64Encoded: false,
269270
};
270271
}
272+
case "route": {
273+
const cacheControl = await computeCacheControl(
274+
localizedPath,
275+
cachedData.value.body,
276+
host,
277+
cachedData.value.revalidate,
278+
cachedData.lastModified,
279+
);
280+
281+
const isBinary = isBinaryContentType(
282+
String(cachedData.value.meta?.headers?.["content-type"]),
283+
);
284+
285+
return {
286+
type: "core",
287+
statusCode: cachedData.value.meta?.status ?? 200,
288+
body: toReadableStream(cachedData.value.body, isBinary),
289+
headers: {
290+
...cacheControl,
291+
...cachedData.value.meta?.headers,
292+
vary: VARY_HEADER,
293+
},
294+
isBase64Encoded: isBinary,
295+
};
296+
}
271297
default:
272298
return event;
273299
}

packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,159 @@ describe("cacheInterceptor", () => {
278278

279279
expect(result).toEqual(event);
280280
});
281+
282+
it("should retrieve route content from cache with text content", async () => {
283+
const event = createEvent({
284+
url: "/albums",
285+
});
286+
const routeBody = JSON.stringify({ message: "Hello from API" });
287+
incrementalCache.get.mockResolvedValueOnce({
288+
value: {
289+
type: "route",
290+
body: routeBody,
291+
meta: {
292+
status: 200,
293+
headers: {
294+
"content-type": "application/json",
295+
},
296+
},
297+
revalidate: 300,
298+
},
299+
lastModified: new Date("2024-01-02T00:00:00Z").getTime(),
300+
});
301+
302+
const result = await cacheInterceptor(event);
303+
304+
const body = await fromReadableStream(result.body);
305+
expect(body).toEqual(routeBody);
306+
expect(result).toEqual(
307+
expect.objectContaining({
308+
type: "core",
309+
statusCode: 200,
310+
isBase64Encoded: false,
311+
headers: expect.objectContaining({
312+
"cache-control": "s-maxage=300, stale-while-revalidate=2592000",
313+
"content-type": "application/json",
314+
etag: expect.any(String),
315+
"x-opennext-cache": "HIT",
316+
vary: "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url",
317+
}),
318+
}),
319+
);
320+
});
321+
322+
it("should retrieve route content from cache with binary content", async () => {
323+
const event = createEvent({
324+
url: "/albums",
325+
});
326+
const routeBody = "randomBinaryData";
327+
incrementalCache.get.mockResolvedValueOnce({
328+
value: {
329+
type: "route",
330+
body: routeBody,
331+
meta: {
332+
status: 200,
333+
headers: {
334+
"content-type": "image/png",
335+
},
336+
},
337+
revalidate: false,
338+
},
339+
lastModified: new Date("2024-01-02T00:00:00Z").getTime(),
340+
});
341+
342+
const result = await cacheInterceptor(event);
343+
344+
const body = await fromReadableStream(result.body, true);
345+
expect(body).toEqual(routeBody);
346+
expect(result).toEqual(
347+
expect.objectContaining({
348+
type: "core",
349+
statusCode: 200,
350+
isBase64Encoded: true,
351+
headers: expect.objectContaining({
352+
"cache-control": "s-maxage=31536000, stale-while-revalidate=2592000",
353+
"content-type": "image/png",
354+
etag: expect.any(String),
355+
"x-opennext-cache": "HIT",
356+
vary: "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url",
357+
}),
358+
}),
359+
);
360+
});
361+
362+
it("should retrieve route content from stale cache", async () => {
363+
const event = createEvent({
364+
url: "/albums",
365+
});
366+
const routeBody = "API response";
367+
incrementalCache.get.mockResolvedValueOnce({
368+
value: {
369+
type: "route",
370+
body: routeBody,
371+
meta: {
372+
status: 201,
373+
headers: {
374+
"content-type": "text/plain",
375+
"custom-header": "custom-value",
376+
},
377+
},
378+
revalidate: 60,
379+
},
380+
lastModified: new Date("2024-01-01T23:58:00Z").getTime(),
381+
});
382+
383+
const result = await cacheInterceptor(event);
384+
385+
const body = await fromReadableStream(result.body);
386+
expect(body).toEqual(routeBody);
387+
expect(result).toEqual(
388+
expect.objectContaining({
389+
type: "core",
390+
statusCode: 201,
391+
isBase64Encoded: false,
392+
headers: expect.objectContaining({
393+
"cache-control": "s-maxage=1, stale-while-revalidate=2592000",
394+
"content-type": "text/plain",
395+
"custom-header": "custom-value",
396+
etag: expect.any(String),
397+
"x-opennext-cache": "STALE",
398+
vary: "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url",
399+
}),
400+
}),
401+
);
402+
});
403+
404+
it("should retrieve route content with default status code when meta is missing", async () => {
405+
const event = createEvent({
406+
url: "/albums",
407+
});
408+
const routeBody = "Simple response";
409+
incrementalCache.get.mockResolvedValueOnce({
410+
value: {
411+
type: "route",
412+
body: routeBody,
413+
revalidate: false,
414+
},
415+
lastModified: new Date("2024-01-02T00:00:00Z").getTime(),
416+
});
417+
418+
const result = await cacheInterceptor(event);
419+
420+
const body = await fromReadableStream(result.body);
421+
expect(body).toEqual(routeBody);
422+
expect(result).toEqual(
423+
expect.objectContaining({
424+
type: "core",
425+
statusCode: 200,
426+
isBase64Encoded: false,
427+
headers: expect.objectContaining({
428+
"cache-control": "s-maxage=31536000, stale-while-revalidate=2592000",
429+
etag: expect.any(String),
430+
"x-opennext-cache": "HIT",
431+
vary: "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url",
432+
}),
433+
}),
434+
);
435+
});
281436
});

0 commit comments

Comments
 (0)