Skip to content

Commit 2fcb6c8

Browse files
Fix missing CORS header on image-cdn's actual image responses
The OPTIONS preflight handler set Access-Control-Allow-Origin, but the real GET responses never did. small/large (served via R2Service) had no CORS header at all, so cross-origin fetch() calls - both the PDF live preview and, from within the PDF render Worker, any consumer fetching these images - failed outright in every browser (confirmed via an isolated main-thread + Worker fetch test in real Firefox and Chromium, no test-harness CORS shim). full happened to "work" only because Google's own response carries a permissive CORS header that passes through unmodified - not something to depend on. Add the header to every actual image response, and add regression tests covering all three size tiers against a mocked upstream response that deliberately has no CORS header of its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 1ca157a commit 2fcb6c8

2 files changed

Lines changed: 44 additions & 15 deletions

File tree

‎image-cdn/src/handler/image.ts‎

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,34 @@ export const handleImageRequest = async (url: URL, request: Request, env: Env, c
2424

2525
const imageKey = R2Service.getImageKey(imageType, imageSize, imageIdentifier);
2626

27-
switch (request.method) {
28-
case "GET":
29-
switch (imageSize) {
30-
case "small":
31-
case "large":
32-
return R2Service.getThumbnail(env, ctx, getImageURL(imageType, imageSize, undefined, jpgQuality, imageIdentifier), imageKey);
33-
case "full":
34-
const url = getImageURL(imageType, imageSize, dpi, jpgQuality, imageIdentifier);
35-
return fetch(url);
36-
default:
37-
throw new Error(`Invalid image size ${imageSize}`);
38-
}
39-
default:
40-
return new Response(`Invalid method ${request.method}. GET or PUT expected.`, { status: 400 });
41-
}
27+
const response = await (async () => {
28+
switch (request.method) {
29+
case "GET":
30+
switch (imageSize) {
31+
case "small":
32+
case "large":
33+
return R2Service.getThumbnail(env, ctx, getImageURL(imageType, imageSize, undefined, jpgQuality, imageIdentifier), imageKey);
34+
case "full":
35+
const url = getImageURL(imageType, imageSize, dpi, jpgQuality, imageIdentifier);
36+
return fetch(url);
37+
default:
38+
throw new Error(`Invalid image size ${imageSize}`);
39+
}
40+
default:
41+
return new Response(`Invalid method ${request.method}. GET or PUT expected.`, { status: 400 });
42+
}
43+
})();
44+
45+
// Callers (the browser's main thread, and the PDF renderer's Worker context)
46+
// fetch() these images cross-origin, which requires an explicit CORS header
47+
// on the actual response - the OPTIONS preflight handler alone isn't enough.
48+
// The "full" tier previously worked by accident because Google's own response
49+
// happens to carry a permissive CORS header; don't rely on that.
50+
const headers = new Headers(response.headers);
51+
headers.set("Access-Control-Allow-Origin", "*");
52+
return new Response(response.body, {
53+
status: response.status,
54+
statusText: response.statusText,
55+
headers,
56+
});
4257
};

‎image-cdn/tests/handler/image.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ describe("worker image routing", () => {
103103
expect(fetchMock).toHaveBeenCalledWith("https://lh4.googleusercontent.com/d/image-id=h2220-rj-l95");
104104
});
105105

106+
it.each(["small", "large", "full"] as const)(
107+
"sets Access-Control-Allow-Origin on %s image responses regardless of the upstream source's own headers",
108+
async (imageSize) => {
109+
// the upstream response deliberately carries no CORS header of its own, so this
110+
// only passes if the worker adds one itself rather than relying on a passthrough.
111+
const fetchMock = vi.fn(async () => new Response("image-bytes", { headers: { "content-length": "11" } }));
112+
vi.stubGlobal("fetch", fetchMock);
113+
114+
const response = await fetchWorker(`http://example.com/images/google_drive/${imageSize}/image-id.jpg`);
115+
116+
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
117+
}
118+
);
119+
106120
it("throws for invalid dpi or JPG quality query parameters", async () => {
107121
await expect(fetchWorker("http://example.com/images/google_drive/full/image-id.jpg?dpi=0")).rejects.toThrow("invalid DPI 0");
108122
await expect(fetchWorker("http://example.com/images/google_drive/full/image-id.jpg?jpgQuality=101")).rejects.toThrow(

0 commit comments

Comments
 (0)