Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 1875c89

Browse files
authored
fix(core, lambda-at-edge) Serve original cached image headers from file system (#2422)
1 parent acfe76e commit 1875c89

File tree

2 files changed

+87
-47
lines changed

2 files changed

+87
-47
lines changed

packages/libs/core/src/images/imageOptimizer.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ export async function imageOptimizer(
212212

213213
const hash = getHash([CACHE_VERSION, href, width, quality, mimeType]);
214214
const imagesDir = join("/tmp", "cache", "images"); // Use Lambda tmp directory
215+
const imagesMetaDir = join("/tmp", "cache", "imageMeta");
215216
const hashDir = join(imagesDir, hash);
217+
const metaDir = join(imagesMetaDir, hash);
216218
const now = Date.now();
217219

218220
if (fs.existsSync(hashDir)) {
@@ -223,8 +225,15 @@ export async function imageOptimizer(
223225
const contentType = getContentType(extension);
224226
const fsPath = join(hashDir, file);
225227
if (now < expireAt) {
228+
const meta = JSON.parse(
229+
(await promises.readFile(join(metaDir, `${file}.json`))).toString()
230+
);
226231
if (!res.getHeader("Cache-Control")) {
227-
res.setHeader("Cache-Control", "public, max-age=60");
232+
if (meta.headers["Cache-Control"]) {
233+
res.setHeader("Cache-Control", meta.headers["Cache-Control"]);
234+
} else {
235+
res.setHeader("Cache-Control", "public, max-age=60");
236+
}
228237
}
229238
if (sendEtagResponse(req, res, etag)) {
230239
return { finished: true };
@@ -243,6 +252,7 @@ export async function imageOptimizer(
243252
let upstreamBuffer: Buffer | undefined;
244253
let upstreamType: string | undefined;
245254
let maxAge: number;
255+
let cacheControl: string | undefined | null;
246256

247257
if (isAbsolute) {
248258
const upstreamRes = await fetch(href);
@@ -256,12 +266,10 @@ export async function imageOptimizer(
256266
res.statusCode = upstreamRes.status;
257267
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
258268
upstreamType = upstreamRes.headers.get("Content-Type") ?? undefined;
259-
maxAge = getMaxAge(upstreamRes.headers.get("Cache-Control") ?? undefined);
260-
if (upstreamRes.headers.get("Cache-Control")) {
261-
res.setHeader(
262-
"Cache-Control",
263-
upstreamRes.headers.get("Cache-Control") as string
264-
);
269+
cacheControl = upstreamRes.headers.get("Cache-Control");
270+
maxAge = getMaxAge(cacheControl ?? undefined);
271+
if (cacheControl) {
272+
res.setHeader("Cache-Control", cacheControl as string);
265273
}
266274
} else {
267275
let objectKey;
@@ -284,6 +292,7 @@ export async function imageOptimizer(
284292

285293
upstreamBuffer = response.body ?? Buffer.of();
286294
upstreamType = response.contentType ?? undefined;
295+
cacheControl = response.cacheControl;
287296
maxAge = getMaxAge(response.cacheControl);
288297

289298
// If object response provides cache control header, use that
@@ -357,11 +366,22 @@ export async function imageOptimizer(
357366
}
358367

359368
const optimizedBuffer = await transformer.toBuffer();
360-
await promises.mkdir(hashDir, { recursive: true });
369+
await Promise.all([
370+
promises.mkdir(hashDir, { recursive: true }),
371+
promises.mkdir(metaDir, { recursive: true })
372+
]);
361373
const extension = getExtension(contentType);
362374
const etag = getHash([optimizedBuffer]);
363-
const filename = join(hashDir, `${expireAt}.${etag}.${extension}`);
364-
await promises.writeFile(filename, optimizedBuffer);
375+
const fileName = `${expireAt}.${etag}.${extension}`;
376+
const filePath = join(hashDir, fileName);
377+
const metaFilename = join(metaDir, `${fileName}.json`);
378+
await Promise.all([
379+
promises.writeFile(filePath, optimizedBuffer),
380+
promises.writeFile(
381+
metaFilename,
382+
JSON.stringify({ headers: { "Cache-Control": cacheControl } })
383+
)
384+
]);
365385
sendResponse(req, res, contentType, optimizedBuffer);
366386
} catch (error: any) {
367387
console.error(

packages/libs/core/tests/images/imageOptimizer.test.ts

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import sharp from "sharp";
2-
import { ImagesManifest } from "../../src";
2+
import { ImagesManifest, PlatformClient } from "../../src";
33
import { imageOptimizer } from "../../src/images/imageOptimizer";
44
import imagesManifest from "./image-images-manifest.json";
5+
import fs from "fs";
56
import url from "url";
67
import http from "http";
78
import Stream from "stream";
8-
import { PlatformClient } from "../../src";
99
import { jest } from "@jest/globals";
1010

1111
jest.mock("node-fetch", () => require("fetch-mock-jest").sandbox());
@@ -103,7 +103,7 @@ describe("Image optimizer", () => {
103103
);
104104
};
105105

106-
beforeEach(async () => {
106+
const setupPlatformClientResponse = async (cacheControlHeader?: string) => {
107107
const imageBuffer: Buffer = await sharp({
108108
create: {
109109
width: 100,
@@ -122,9 +122,14 @@ describe("Image optimizer", () => {
122122
expires: undefined,
123123
eTag: "etag",
124124
statusCode: 200,
125-
cacheControl: undefined,
125+
cacheControl: cacheControlHeader,
126126
contentType: "image/png"
127127
});
128+
};
129+
130+
beforeEach(() => {
131+
fs.rmSync("/tmp/cache/images", { recursive: true, force: true });
132+
fs.rmSync("/tmp/cache/imageMeta", { recursive: true, force: true });
128133
});
129134

130135
describe("Routes", () => {
@@ -137,6 +142,7 @@ describe("Image optimizer", () => {
137142
`(
138143
"serves image request",
139144
async ({ imagePath, accept, expectedObjectKey }) => {
145+
await setupPlatformClientResponse();
140146
const { parsedUrl, req, res } = createEventByImagePath(imagePath, {
141147
accept: accept
142148
});
@@ -162,47 +168,61 @@ describe("Image optimizer", () => {
162168
);
163169

164170
it.each`
165-
imagePath
166-
${"/test-image-cached.png"}
167-
`("serves cached image on second request", async ({ imagePath }) => {
168-
const {
169-
parsedUrl: parsedUrl1,
170-
req: req1,
171-
res: res1
172-
} = createEventByImagePath(imagePath);
173-
const {
174-
parsedUrl: parsedUrl2,
175-
req: req2,
176-
res: res2
177-
} = createEventByImagePath(imagePath);
171+
imagePath | cacheControlHeader
172+
${"/test-image-cached.png"} | ${undefined}
173+
${"/test-image-cached.png"} | ${"public,max-age=31536000,immutable"}
174+
`(
175+
"serves cached image on second request with $cacheControlHeader cache header",
176+
async ({ imagePath, cacheControlHeader }) => {
177+
await setupPlatformClientResponse(cacheControlHeader);
178+
const {
179+
parsedUrl: parsedUrl1,
180+
req: req1,
181+
res: res1
182+
} = createEventByImagePath(imagePath);
183+
const {
184+
parsedUrl: parsedUrl2,
185+
req: req2,
186+
res: res2
187+
} = createEventByImagePath(imagePath);
178188

179-
await imageOptimizer(
180-
"",
181-
imagesManifest as ImagesManifest,
182-
req1,
183-
res1,
184-
parsedUrl1,
185-
mockPlatformClient as PlatformClient
186-
);
187-
await imageOptimizer(
188-
"",
189-
imagesManifest as ImagesManifest,
190-
req2,
191-
res2,
192-
parsedUrl2,
193-
mockPlatformClient as PlatformClient
194-
);
189+
await imageOptimizer(
190+
"",
191+
imagesManifest as ImagesManifest,
192+
req1,
193+
res1,
194+
parsedUrl1,
195+
mockPlatformClient as PlatformClient
196+
);
197+
await imageOptimizer(
198+
"",
199+
imagesManifest as ImagesManifest,
200+
req2,
201+
res2,
202+
parsedUrl2,
203+
mockPlatformClient as PlatformClient
204+
);
195205

196-
expect(res1.statusCode).toEqual(200);
197-
expect(res2.statusCode).toEqual(200);
206+
expect(res1.statusCode).toEqual(200);
207+
expect(res2.statusCode).toEqual(200);
198208

199-
expect(mockPlatformClient.getObject).toBeCalledTimes(1);
200-
});
209+
let defaultCacheHeader = "public, max-age=60";
210+
expect(res1.headers["cache-control"]).toEqual(
211+
cacheControlHeader ?? defaultCacheHeader
212+
);
213+
expect(res2.headers["cache-control"]).toEqual(
214+
cacheControlHeader ?? defaultCacheHeader
215+
);
216+
217+
expect(mockPlatformClient.getObject).toBeCalledTimes(1);
218+
}
219+
);
201220

202221
it.each`
203222
imagePath
204223
${"/test-image-etag.png"}
205224
`("serves 304 when etag matches", async ({ imagePath }) => {
225+
await setupPlatformClientResponse();
206226
const {
207227
parsedUrl: parsedUrl1,
208228
req: req1,

0 commit comments

Comments
 (0)