···1919 * Local images (starting with a '/' as fetched using the passed fetcher).
2020 * Remote images should match the configured remote patterns or a 404 response is returned.
2121 */
2222-export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) {
2222+export async function fetchImage(fetcher: Fetcher | undefined, imageUrl: string, ctx: ExecutionContext) {
2323 // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
2424 if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) {
2525 return getUrlErrorResponse();
···6969 return getUrlErrorResponse();
7070 }
71717272- return fetch(imageUrl, { cf: { cacheEverything: true } });
7272+ const imgResponse = await fetch(imageUrl, { cf: { cacheEverything: true } });
7373+7474+ if (!imgResponse.body) {
7575+ return imgResponse;
7676+ }
7777+7878+ const buffer = new ArrayBuffer(32);
7979+8080+ try {
8181+ let contentType: string | undefined;
8282+ // body1 is eventually used for the response
8383+ // body2 is used to detect the content type
8484+ const [body1, body2] = imgResponse.body.tee();
8585+ const reader = body2.getReader({ mode: "byob" });
8686+ const { value } = await reader.read(new Uint8Array(buffer));
8787+ // Release resources by calling `reader.cancel()`
8888+ // `ctx.waitUntil` keeps the runtime running until the promise settles without having to wait here.
8989+ ctx.waitUntil(reader.cancel());
9090+9191+ if (value) {
9292+ contentType = detectContentType(value);
9393+ }
9494+9595+ if (contentType && !(contentType === SVG && !__IMAGES_ALLOW_SVG__)) {
9696+ const headers = new Headers(imgResponse.headers);
9797+ headers.set("content-type", contentType);
9898+ headers.set("content-disposition", __IMAGES_CONTENT_DISPOSITION__);
9999+ headers.set("content-security-policy", __IMAGES_CONTENT_SECURITY_POLICY__);
100100+ return new Response(body1, { ...imgResponse, headers });
101101+ }
102102+103103+ return new Response('"url" parameter is valid but image type is not allowed', {
104104+ status: 400,
105105+ });
106106+ } catch {
107107+ return new Response('"url" parameter is valid but upstream response is invalid', {
108108+ status: 400,
109109+ });
110110+ }
73111}
7411275113export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
···113151 return new Response(`"url" parameter is not allowed`, { status: 400 });
114152}
115153154154+const AVIF = "image/avif";
155155+const WEBP = "image/webp";
156156+const PNG = "image/png";
157157+const JPEG = "image/jpeg";
158158+const GIF = "image/gif";
159159+const SVG = "image/svg+xml";
160160+const ICO = "image/x-icon";
161161+const ICNS = "image/x-icns";
162162+const TIFF = "image/tiff";
163163+const BMP = "image/bmp";
164164+165165+/**
166166+ * Detects the content type by looking at the first few bytes of a file
167167+ *
168168+ * Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
169169+ *
170170+ * @param buffer The image bytes
171171+ * @returns a content type of undefined for unsupported content
172172+ */
173173+export function detectContentType(buffer: Uint8Array) {
174174+ if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
175175+ return JPEG;
176176+ }
177177+ if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
178178+ return PNG;
179179+ }
180180+ if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
181181+ return GIF;
182182+ }
183183+ if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
184184+ return WEBP;
185185+ }
186186+ if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
187187+ return SVG;
188188+ }
189189+ if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
190190+ return SVG;
191191+ }
192192+ if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
193193+ return AVIF;
194194+ }
195195+ if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
196196+ return ICO;
197197+ }
198198+ if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) {
199199+ return ICNS;
200200+ }
201201+ if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) {
202202+ return TIFF;
203203+ }
204204+ if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) {
205205+ return BMP;
206206+ }
207207+}
208208+116209/* eslint-disable no-var */
117210declare global {
118211 var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
119212 var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
213213+ var __IMAGES_ALLOW_SVG__: boolean;
214214+ var __IMAGES_CONTENT_SECURITY_POLICY__: string;
215215+ var __IMAGES_CONTENT_DISPOSITION__: string;
120216}
121217/* eslint-enable no-var */
+1-1
packages/cloudflare/src/cli/templates/worker.ts
···4141 // Fallback for the Next default image loader.
4242 if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) {
4343 const imageUrl = url.searchParams.get("url") ?? "";
4444- return fetchImage(env.ASSETS, imageUrl);
4444+ return await fetchImage(env.ASSETS, imageUrl, ctx);
4545 }
46464747 // - `Request`s are handled by the Next server