···11+import { error, warn } from "@opennextjs/aws/adapters/logger.js";
22+13export type RemotePattern = {
24 protocol?: "http" | "https";
35 hostname: string;
···1315 search?: string;
1416};
15171616-let NEXT_IMAGE_REGEXP: RegExp;
1717-1818/**
1919- * Fetches an images.
1919+ * Handles requests to /_next/image(/), including image optimizations.
2020 *
2121- * Local images (starting with a '/' as fetched using the passed fetcher).
2222- * Remote images should match the configured remote patterns or a 404 response is returned.
2121+ * Image optimization is disabled and the original image is returned if `env.IMAGES` is undefined.
2222+ *
2323+ * Throws an exception on unexpected errors.
2424+ *
2525+ * @param requestURL
2626+ * @param requestHeaders
2727+ * @param env
2828+ * @returns A promise that resolves to the resolved request.
2329 */
2424-export async function fetchImage(fetcher: Fetcher | undefined, imageUrl: string, ctx: ExecutionContext) {
2525- // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
2626- if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) {
2727- return getUrlErrorResponse();
3030+export async function handleImageRequest(
3131+ requestURL: URL,
3232+ requestHeaders: Headers,
3333+ env: CloudflareEnv
3434+): Promise<Response> {
3535+ const parseResult = parseImageRequest(requestURL, requestHeaders);
3636+ if (!parseResult.ok) {
3737+ return new Response(parseResult.message, {
3838+ status: 400,
3939+ });
2840 }
29413030- // Local
3131- if (imageUrl.startsWith("/")) {
3232- // @ts-expect-error TS2339 Missing types for URL.parse
3333- const url = URL.parse(imageUrl, "http://n");
4242+ let imageResponse: Response;
4343+ if (parseResult.url.startsWith("/")) {
4444+ if (env.ASSETS === undefined) {
4545+ error("env.ASSETS binding is not defined");
4646+ return new Response('"url" parameter is valid but upstream response is invalid', {
4747+ status: 404,
4848+ });
4949+ }
5050+ const absoluteURL = new URL(parseResult.url, requestURL);
5151+ imageResponse = await env.ASSETS.fetch(absoluteURL);
5252+ } else {
5353+ let fetchImageResult: FetchWithRedirectsResult;
5454+ try {
5555+ fetchImageResult = await fetchWithRedirects(parseResult.url, 7_000, __IMAGES_MAX_REDIRECTS__);
5656+ } catch (e) {
5757+ throw new Error("Failed to fetch image", { cause: e });
5858+ }
5959+ if (!fetchImageResult.ok) {
6060+ if (fetchImageResult.error === "timed_out") {
6161+ return new Response('"url" parameter is valid but upstream response timed out', {
6262+ status: 504,
6363+ });
6464+ }
6565+ if (fetchImageResult.error === "too_many_redirects") {
6666+ return new Response('"url" parameter is valid but upstream response is invalid', {
6767+ status: 508,
6868+ });
6969+ }
7070+ throw new Error("Failed to fetch image");
7171+ }
7272+ imageResponse = fetchImageResult.response;
7373+ }
34743535- if (url == null) {
3636- return getUrlErrorResponse();
7575+ if (!imageResponse.ok || imageResponse.body === null) {
7676+ return new Response('"url" parameter is valid but upstream response is invalid', {
7777+ status: imageResponse.status,
7878+ });
7979+ }
8080+8181+ let immutable = false;
8282+ if (parseResult.static) {
8383+ immutable = true;
8484+ } else {
8585+ const cacheControlHeader = imageResponse.headers.get("Cache-Control");
8686+ if (cacheControlHeader !== null) {
8787+ // TODO: Properly parse header
8888+ immutable = cacheControlHeader.includes("immutable");
3789 }
9090+ }
38913939- // This method will never throw because URL parser will handle invalid input.
4040- const pathname = decodeURIComponent(url.pathname);
9292+ const [contentTypeImageStream, imageStream] = imageResponse.body.tee();
9393+ const imageHeaderBytes = new Uint8Array(32);
9494+ const contentTypeImageReader = contentTypeImageStream.getReader({
9595+ mode: "byob",
9696+ });
9797+ const readImageHeaderBytesResult = await contentTypeImageReader.readAtLeast(32, imageHeaderBytes);
9898+ if (readImageHeaderBytesResult.value === undefined) {
9999+ await imageResponse.body.cancel();
411004242- NEXT_IMAGE_REGEXP ??= /\/_next\/image($|\/)/;
4343- if (NEXT_IMAGE_REGEXP.test(pathname)) {
4444- return getUrlErrorResponse();
101101+ return new Response('"url" parameter is valid but upstream response is invalid', {
102102+ status: 400,
103103+ });
104104+ }
105105+ const contentType = detectImageContentType(readImageHeaderBytesResult.value);
106106+ if (contentType === null) {
107107+ warn(`Failed to detect content type of "${parseResult.url}"`);
108108+ return new Response('"url" parameter is valid but image type is not allowed', {
109109+ status: 400,
110110+ });
111111+ }
112112+ if (contentType === SVG) {
113113+ if (!__IMAGES_ALLOW_SVG__) {
114114+ return new Response('"url" parameter is valid but image type is not allowed', {
115115+ status: 400,
116116+ });
117117+ }
118118+ const response = createImageResponse(imageStream, contentType, {
119119+ immutable,
120120+ });
121121+ return response;
122122+ }
123123+124124+ if (contentType === GIF) {
125125+ if (env.IMAGES === undefined) {
126126+ warn("env.IMAGES binding is not defined");
127127+ const response = createImageResponse(imageStream, contentType, {
128128+ immutable,
129129+ });
130130+ return response;
45131 }
461324747- // If localPatterns are not defined all local images are allowed.
4848- if (
4949- __IMAGES_LOCAL_PATTERNS__.length > 0 &&
5050- !__IMAGES_LOCAL_PATTERNS__.some((p: LocalPattern) => matchLocalPattern(p, url))
5151- ) {
5252- return getUrlErrorResponse();
133133+ const imageSource = env.IMAGES.input(imageStream);
134134+ const imageTransformationResult = await imageSource
135135+ .transform({
136136+ width: parseResult.width,
137137+ fit: "scale-down",
138138+ })
139139+ .output({
140140+ quality: parseResult.quality,
141141+ format: GIF,
142142+ });
143143+ const outputImageStream = imageTransformationResult.image();
144144+ const response = createImageResponse(outputImageStream, GIF, {
145145+ immutable,
146146+ });
147147+ return response;
148148+ }
149149+150150+ if (contentType === AVIF || contentType === WEBP || contentType === JPEG || contentType === PNG) {
151151+ if (env.IMAGES === undefined) {
152152+ warn("env.IMAGES binding is not defined");
153153+ const response = createImageResponse(imageStream, contentType, {
154154+ immutable,
155155+ });
156156+ return response;
53157 }
541585555- return fetcher?.fetch(`http://assets.local${imageUrl}`);
159159+ const outputFormat = parseResult.format ?? contentType;
160160+ const imageSource = env.IMAGES.input(imageStream);
161161+ const imageTransformationResult = await imageSource
162162+ .transform({
163163+ width: parseResult.width,
164164+ fit: "scale-down",
165165+ })
166166+ .output({
167167+ quality: parseResult.quality,
168168+ format: outputFormat,
169169+ });
170170+ const outputImageStream = imageTransformationResult.image();
171171+ const response = createImageResponse(outputImageStream, outputFormat, {
172172+ immutable,
173173+ });
174174+ return response;
56175 }
571765858- // Remote
5959- let url: URL;
177177+ warn(`Image content type ${contentType} not supported`);
178178+179179+ const response = createImageResponse(imageStream, contentType, {
180180+ immutable,
181181+ });
182182+183183+ return response;
184184+}
185185+186186+/**
187187+ * Fetch call with max redirects and timeouts.
188188+ *
189189+ * Re-throws the exception thrown by a fetch call.
190190+ * @param url
191191+ * @param timeoutMS Timeout for a single fetch call.
192192+ * @param maxRedirectCount
193193+ * @returns
194194+ */
195195+async function fetchWithRedirects(
196196+ url: string,
197197+ timeoutMS: number,
198198+ maxRedirectCount: number
199199+): Promise<FetchWithRedirectsResult> {
200200+ // TODO: Add dangerouslyAllowLocalIP support
201201+202202+ let response: Response;
60203 try {
6161- url = new URL(imageUrl);
6262- } catch {
6363- return getUrlErrorResponse();
204204+ response = await fetch(url, {
205205+ signal: AbortSignal.timeout(timeoutMS),
206206+ redirect: "manual",
207207+ });
208208+ } catch (e) {
209209+ if (e instanceof Error && e.name === "TimeoutError") {
210210+ const result: FetchWithRedirectsErrorResult = {
211211+ ok: false,
212212+ error: "timed_out",
213213+ };
214214+ return result;
215215+ }
216216+ throw e;
217217+ }
218218+ if (redirectResponseStatuses.includes(response.status)) {
219219+ const locationHeader = response.headers.get("Location");
220220+ if (locationHeader !== null) {
221221+ if (maxRedirectCount < 1) {
222222+ const result: FetchWithRedirectsErrorResult = {
223223+ ok: false,
224224+ error: "too_many_redirects",
225225+ };
226226+ return result;
227227+ }
228228+ let redirectTarget: string;
229229+ if (locationHeader.startsWith("/")) {
230230+ redirectTarget = new URL(locationHeader, url).href;
231231+ } else {
232232+ redirectTarget = locationHeader;
233233+ }
234234+ const result = await fetchWithRedirects(redirectTarget, timeoutMS, maxRedirectCount - 1);
235235+ return result;
236236+ }
64237 }
238238+ const result: FetchWithRedirectsSuccessResult = {
239239+ ok: true,
240240+ response: response,
241241+ };
242242+ return result;
243243+}
652446666- if (url.protocol !== "http:" && url.protocol !== "https:") {
6767- return getUrlErrorResponse();
6868- }
245245+type FetchWithRedirectsResult = FetchWithRedirectsSuccessResult | FetchWithRedirectsErrorResult;
692467070- // The remotePatterns is used to allow images from specific remote external paths and block all others.
7171- if (!__IMAGES_REMOTE_PATTERNS__.some((p: RemotePattern) => matchRemotePattern(p, url))) {
7272- return getUrlErrorResponse();
247247+type FetchWithRedirectsSuccessResult = {
248248+ ok: true;
249249+ response: Response;
250250+};
251251+252252+type FetchWithRedirectsErrorResult = {
253253+ ok: false;
254254+ error: FetchImageError;
255255+};
256256+257257+type FetchImageError = "timed_out" | "too_many_redirects";
258258+259259+const redirectResponseStatuses = [301, 302, 303, 307, 308];
260260+261261+function createImageResponse(
262262+ image: ReadableStream,
263263+ contentType: string,
264264+ imageResponseFlags: ImageResponseFlags
265265+): Response {
266266+ const response = new Response(image, {
267267+ headers: {
268268+ Vary: "Accept",
269269+ "Content-Type": contentType,
270270+ "Content-Disposition": __IMAGES_CONTENT_DISPOSITION__,
271271+ "Content-Security-Policy": __IMAGES_CONTENT_SECURITY_POLICY__,
272272+ },
273273+ });
274274+ if (imageResponseFlags.immutable) {
275275+ response.headers.set("Cache-Control", "public, max-age=315360000, immutable");
73276 }
277277+ return response;
278278+}
742797575- const imgResponse = await fetch(imageUrl, { cf: { cacheEverything: true } });
280280+type ImageResponseFlags = {
281281+ immutable: boolean;
282282+};
283283+284284+/**
285285+ * Parses the image request URL and headers.
286286+ *
287287+ * This function validates the parameters and returns either the parsed result or an error message.
288288+ *
289289+ * @param requestURL request URL
290290+ * @param requestHeaders request headers
291291+ * @returns an instance of `ParseImageRequestURLSuccessResult` when successful, or an instance of `ErrorResult` when failed.
292292+ */
293293+function parseImageRequest(
294294+ requestURL: URL,
295295+ requestHeaders: Headers
296296+): ParseImageRequestURLSuccessResult | ErrorResult {
297297+ const formats = __IMAGES_FORMATS__;
762987777- if (!imgResponse.body) {
7878- return imgResponse;
299299+ const parsedUrlOrError = validateUrlQueryParameter(requestURL);
300300+ if (!("url" in parsedUrlOrError)) {
301301+ return parsedUrlOrError;
79302 }
803038181- const buffer = new ArrayBuffer(32);
304304+ const widthOrError = validateWidthQueryParameter(requestURL);
305305+ if (typeof widthOrError !== "number") {
306306+ return widthOrError;
307307+ }
823088383- try {
8484- let contentType: string | undefined;
8585- // respBody is eventually used for the response
8686- // contentBody is used to detect the content type
8787- const [respBody, contentBody] = imgResponse.body.tee();
8888- const reader = contentBody.getReader({ mode: "byob" });
8989- const { value } = await reader.read(new Uint8Array(buffer));
9090- // Release resources by calling `reader.cancel()`
9191- // `ctx.waitUntil` keeps the runtime running until the promise settles without having to wait here.
9292- ctx.waitUntil(reader.cancel());
309309+ const qualityOrError = validateQualityQueryParameter(requestURL);
310310+ if (typeof qualityOrError !== "number") {
311311+ return qualityOrError;
312312+ }
933139494- if (value) {
9595- contentType = detectContentType(value);
314314+ const acceptHeader = requestHeaders.get("Accept") ?? "";
315315+ let format: OptimizedImageFormat | null = null;
316316+ // Find a more specific format that the client accepts.
317317+ for (const allowedFormat of formats) {
318318+ if (acceptHeader.includes(allowedFormat)) {
319319+ format = allowedFormat;
320320+ break;
96321 }
322322+ }
973239898- if (!contentType) {
9999- // Fallback to upstream header when the type can not be detected
100100- // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L748
101101- contentType = imgResponse.headers.get("content-type") ?? "";
324324+ const result: ParseImageRequestURLSuccessResult = {
325325+ ok: true,
326326+ url: parsedUrlOrError.url,
327327+ width: widthOrError,
328328+ quality: qualityOrError,
329329+ format,
330330+ static: parsedUrlOrError.static,
331331+ };
332332+ return result;
333333+}
334334+335335+type ParseImageRequestURLSuccessResult = {
336336+ ok: true;
337337+ /** Absolute or relative URL. */
338338+ url: string;
339339+ width: number;
340340+ quality: number;
341341+ format: OptimizedImageFormat | null;
342342+ static: boolean;
343343+};
344344+345345+export type OptimizedImageFormat = "image/avif" | "image/webp";
346346+347347+type ErrorResult = {
348348+ ok: false;
349349+ message: string;
350350+};
351351+352352+/**
353353+ * Validates that there is exactly one "url" query parameter.
354354+ *
355355+ * @returns the validated URL or an error result.
356356+ */
357357+function validateUrlQueryParameter(requestURL: URL): ErrorResult | { url: string; static: boolean } {
358358+ // There should be a single "url" parameter.
359359+ const urls = requestURL.searchParams.getAll("url");
360360+ if (urls.length < 1) {
361361+ const result: ErrorResult = {
362362+ ok: false,
363363+ message: '"url" parameter is required',
364364+ };
365365+ return result;
366366+ }
367367+ if (urls.length > 1) {
368368+ const result: ErrorResult = {
369369+ ok: false,
370370+ message: '"url" parameter cannot be an array',
371371+ };
372372+ return result;
373373+ }
374374+375375+ // The url parameter value should be a valid URL or a valid relative URL.
376376+ const url = urls[0]!;
377377+ if (url.length > 3072) {
378378+ const result: ErrorResult = {
379379+ ok: false,
380380+ message: '"url" parameter is too long',
381381+ };
382382+ return result;
383383+ }
384384+ if (url.startsWith("//")) {
385385+ const result: ErrorResult = {
386386+ ok: false,
387387+ message: '"url" parameter cannot be a protocol-relative URL (//)',
388388+ };
389389+ return result;
390390+ }
391391+392392+ if (url.startsWith("/")) {
393393+ const staticAsset = url.startsWith(`${__NEXT_BASE_PATH__ || ""}/_next/static/media`);
394394+395395+ const pathname = getPathnameFromRelativeURL(url);
396396+ if (/\/_next\/image($|\/)/.test(decodeURIComponent(pathname))) {
397397+ const result: ErrorResult = {
398398+ ok: false,
399399+ message: '"url" parameter cannot be recursive',
400400+ };
401401+ return result;
102402 }
103403104104- // Sanitize the content type:
105105- // - Accept images only
106106- // - Reject multiple content types
107107- if (!contentType.startsWith("image/") || contentType.includes(",")) {
108108- contentType = undefined;
404404+ if (!staticAsset) {
405405+ if (!hasLocalMatch(__IMAGES_LOCAL_PATTERNS__, url)) {
406406+ const result: ErrorResult = { ok: false, message: '"url" parameter is not allowed' };
407407+ return result;
408408+ }
109409 }
110410111111- if (contentType && !(contentType === SVG && !__IMAGES_ALLOW_SVG__)) {
112112- const headers = new Headers(imgResponse.headers);
113113- headers.set("content-type", contentType);
114114- headers.set("content-disposition", __IMAGES_CONTENT_DISPOSITION__);
115115- headers.set("content-security-policy", __IMAGES_CONTENT_SECURITY_POLICY__);
116116- return new Response(respBody, { ...imgResponse, headers });
411411+ return { url, static: staticAsset };
412412+ }
413413+414414+ let parsedURL: URL;
415415+ try {
416416+ parsedURL = new URL(url);
417417+ } catch {
418418+ const result: ErrorResult = { ok: false, message: '"url" parameter is invalid' };
419419+ return result;
420420+ }
421421+422422+ const validProtocols = ["http:", "https:"];
423423+ if (!validProtocols.includes(parsedURL.protocol)) {
424424+ const result: ErrorResult = {
425425+ ok: false,
426426+ message: '"url" parameter is invalid',
427427+ };
428428+ return result;
429429+ }
430430+ if (!hasRemoteMatch(__IMAGES_REMOTE_PATTERNS__, parsedURL)) {
431431+ const result: ErrorResult = {
432432+ ok: false,
433433+ message: '"url" parameter is not allowed',
434434+ };
435435+ return result;
436436+ }
437437+438438+ return { url: parsedURL.href, static: false };
439439+}
440440+441441+/**
442442+ * Validates the "w" (width) query parameter.
443443+ *
444444+ * @returns the validated width number or an error result.
445445+ */
446446+function validateWidthQueryParameter(requestURL: URL): ErrorResult | number {
447447+ const widthQueryValues = requestURL.searchParams.getAll("w");
448448+ if (widthQueryValues.length < 1) {
449449+ const result: ErrorResult = {
450450+ ok: false,
451451+ message: '"w" parameter (width) is required',
452452+ };
453453+ return result;
454454+ }
455455+ if (widthQueryValues.length > 1) {
456456+ const result: ErrorResult = {
457457+ ok: false,
458458+ message: '"w" parameter (width) cannot be an array',
459459+ };
460460+ return result;
461461+ }
462462+ const widthQueryValue = widthQueryValues[0]!;
463463+ if (!/^[0-9]+$/.test(widthQueryValue)) {
464464+ const result: ErrorResult = {
465465+ ok: false,
466466+ message: '"w" parameter (width) must be an integer greater than 0',
467467+ };
468468+ return result;
469469+ }
470470+ const width = parseInt(widthQueryValue, 10);
471471+ if (width <= 0 || isNaN(width)) {
472472+ const result: ErrorResult = {
473473+ ok: false,
474474+ message: '"w" parameter (width) must be an integer greater than 0',
475475+ };
476476+ return result;
477477+ }
478478+479479+ const sizeValid = __IMAGES_DEVICE_SIZES__.includes(width) || __IMAGES_IMAGE_SIZES__.includes(width);
480480+ if (!sizeValid) {
481481+ const result: ErrorResult = {
482482+ ok: false,
483483+ message: `"w" parameter (width) of ${width} is not allowed`,
484484+ };
485485+ return result;
486486+ }
487487+488488+ return width;
489489+}
490490+491491+/**
492492+ * Validates the "q" (quality) query parameter.
493493+ *
494494+ * @returns the validated quality number or an error result.
495495+ */
496496+function validateQualityQueryParameter(requestURL: URL): ErrorResult | number {
497497+ const qualityQueryValues = requestURL.searchParams.getAll("q");
498498+ if (qualityQueryValues.length < 1) {
499499+ const result: ErrorResult = {
500500+ ok: false,
501501+ message: '"q" parameter (quality) is required',
502502+ };
503503+ return result;
504504+ }
505505+ if (qualityQueryValues.length > 1) {
506506+ const result: ErrorResult = {
507507+ ok: false,
508508+ message: '"q" parameter (quality) cannot be an array',
509509+ };
510510+ return result;
511511+ }
512512+ const qualityQueryValue = qualityQueryValues[0]!;
513513+ if (!/^[0-9]+$/.test(qualityQueryValue)) {
514514+ const result: ErrorResult = {
515515+ ok: false,
516516+ message: '"q" parameter (quality) must be an integer between 1 and 100',
517517+ };
518518+ return result;
519519+ }
520520+ const quality = parseInt(qualityQueryValue, 10);
521521+ if (isNaN(quality) || quality < 1 || quality > 100) {
522522+ const result: ErrorResult = {
523523+ ok: false,
524524+ message: '"q" parameter (quality) must be an integer between 1 and 100',
525525+ };
526526+ return result;
527527+ }
528528+ if (!__IMAGES_QUALITIES__.includes(quality)) {
529529+ const result: ErrorResult = {
530530+ ok: false,
531531+ message: `"q" parameter (quality) of ${quality} is not allowed`,
532532+ };
533533+ return result;
534534+ }
535535+536536+ return quality;
537537+}
538538+539539+function getPathnameFromRelativeURL(relativeURL: string): string {
540540+ return relativeURL.split("?")[0]!;
541541+}
542542+543543+function hasLocalMatch(localPatterns: LocalPattern[], relativeURL: string): boolean {
544544+ const parseRelativeURLResult = parseRelativeURL(relativeURL);
545545+ for (const localPattern of localPatterns) {
546546+ const matched = matchLocalPattern(localPattern, parseRelativeURLResult);
547547+ if (matched) {
548548+ return true;
117549 }
550550+ }
551551+ return false;
552552+}
118553119119- // Cancel the unused stream
120120- ctx.waitUntil(respBody.cancel());
554554+function parseRelativeURL(relativeURL: string): ParseRelativeURLResult {
555555+ if (!relativeURL.includes("?")) {
556556+ const result: ParseRelativeURLResult = {
557557+ pathname: relativeURL,
558558+ search: "",
559559+ };
560560+ return result;
561561+ }
562562+ const parts = relativeURL.split("?");
563563+ const pathname = parts[0]!;
564564+ const search = "?" + parts.slice(1).join("?");
565565+ const result: ParseRelativeURLResult = {
566566+ pathname,
567567+ search,
568568+ };
569569+ return result;
570570+}
121571122122- return new Response('"url" parameter is valid but image type is not allowed', {
123123- status: 400,
124124- });
125125- } catch {
126126- return new Response('"url" parameter is valid but upstream response is invalid', {
127127- status: 400,
128128- });
572572+type ParseRelativeURLResult = {
573573+ pathname: string;
574574+ search: string;
575575+};
576576+577577+export function matchLocalPattern(pattern: LocalPattern, url: { pathname: string; search: string }): boolean {
578578+ if (pattern.search !== undefined && pattern.search !== url.search) {
579579+ return false;
129580 }
581581+582582+ return new RegExp(pattern.pathname).test(url.pathname);
583583+}
584584+585585+function hasRemoteMatch(remotePatterns: RemotePattern[], url: URL): boolean {
586586+ for (const remotePattern of remotePatterns) {
587587+ const matched = matchRemotePattern(remotePattern, url);
588588+ if (matched) {
589589+ return true;
590590+ }
591591+ }
592592+ return false;
130593}
131594132595export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
···154617 return new RegExp(pattern.pathname).test(url.pathname);
155618}
156619157157-export function matchLocalPattern(pattern: LocalPattern, url: URL): boolean {
158158- // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-local-pattern.ts
159159- if (pattern.search !== undefined && pattern.search !== url.search) {
160160- return false;
161161- }
162162-163163- return new RegExp(pattern.pathname).test(url.pathname);
164164-}
165165-166166-/**
167167- * @returns same error as Next.js when the url query parameter is not accepted.
168168- */
169169-function getUrlErrorResponse() {
170170- return new Response(`"url" parameter is not allowed`, { status: 400 });
171171-}
172172-173620const AVIF = "image/avif";
174621const WEBP = "image/webp";
175622const PNG = "image/png";
···183630const ICNS = "image/x-icns";
184631const TIFF = "image/tiff";
185632const BMP = "image/bmp";
186186-// pdf will be rejected (not an `image/...` type)
187187-const PDF = "application/pdf";
633633+634634+type ImageContentType =
635635+ | "image/avif"
636636+ | "image/webp"
637637+ | "image/png"
638638+ | "image/jpeg"
639639+ | "image/jxl"
640640+ | "image/jp2"
641641+ | "image/heic"
642642+ | "image/gif"
643643+ | "image/svg+xml"
644644+ | "image/x-icon"
645645+ | "image/x-icns"
646646+ | "image/tiff"
647647+ | "image/bmp";
188648189649/**
190650 * Detects the content type by looking at the first few bytes of a file
···194654 * @param buffer The image bytes
195655 * @returns a content type of undefined for unsupported content
196656 */
197197-export function detectContentType(buffer: Uint8Array) {
657657+export function detectImageContentType(buffer: Uint8Array): ImageContentType | null {
198658 if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
199659 return JPEG;
200660 }
···239699 if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63].every((b, i) => !b || buffer[i] === b)) {
240700 return HEIC;
241701 }
242242- if ([0x25, 0x50, 0x44, 0x46, 0x2d].every((b, i) => buffer[i] === b)) {
243243- return PDF;
244244- }
245702 if (
246703 [0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)
247704 ) {
248705 return JP2;
249706 }
707707+ return null;
250708}
251709252710declare global {
253711 var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
254712 var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
713713+ var __IMAGES_DEVICE_SIZES__: number[];
714714+ var __IMAGES_IMAGE_SIZES__: number[];
715715+ var __IMAGES_QUALITIES__: number[];
716716+ var __IMAGES_FORMATS__: NextConfigImageFormat[];
717717+ var __IMAGES_MINIMUM_CACHE_TTL_SEC__: number;
255718 var __IMAGES_ALLOW_SVG__: boolean;
256719 var __IMAGES_CONTENT_SECURITY_POLICY__: string;
257720 var __IMAGES_CONTENT_DISPOSITION__: string;
721721+ var __IMAGES_MAX_REDIRECTS__: number;
722722+723723+ type NextConfigImageFormat = "image/avif" | "image/webp";
258724}
+2-4
packages/cloudflare/src/cli/templates/worker.ts
···11//@ts-expect-error: Will be resolved by wrangler build
22-import { fetchImage } from "./cloudflare/images.js";
22+import { handleImageRequest } from "./cloudflare/images.js";
33//@ts-expect-error: Will be resolved by wrangler build
44import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
55//@ts-expect-error: Will be resolved by wrangler build
66import { maybeGetSkewProtectionResponse } from "./cloudflare/skew-protection.js";
77// @ts-expect-error: Will be resolved by wrangler build
88import { handler as middlewareHandler } from "./middleware/handler.mjs";
99-109//@ts-expect-error: Will be resolved by wrangler build
1110export { DOQueueHandler } from "./.build/durable-objects/queue.js";
1211//@ts-expect-error: Will be resolved by wrangler build
···4342 url.pathname ===
4443 `${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}`
4544 ) {
4646- const imageUrl = url.searchParams.get("url") ?? "";
4747- return await fetchImage(env.ASSETS, imageUrl, ctx);
4545+ return await handleImageRequest(url, request.headers, env);
4846 }
49475048 // - `Request`s are handled by the Next server