Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaky-kings-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

feat(images): implement localPatterns for images
38 changes: 31 additions & 7 deletions examples/playground14/e2e/cloudflare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,38 @@ test.describe("playground/cloudflare", () => {
await expect(res.json()).resolves.toEqual(expect.objectContaining({ PROCESS_ENV_VAR: "process.env" }));
});

test("fetch an image allowed by remotePatterns", async ({ page }) => {
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818");
expect(res.status()).toBe(200);
expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" });
test.describe("remotePatterns", () => {
test("fetch an image allowed by remotePatterns", async ({ page }) => {
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818");
expect(res.status()).toBe(200);
expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" });
});

test("400 when fetching an image disallowed by remotePatterns", async ({ page }) => {
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817");
expect(res.status()).toBe(400);
});
});

test("404 when fetching an image disallowed by remotePatterns", async ({ page }) => {
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817");
expect(res.status()).toBe(400);
test.describe("localPatterns", () => {
test("fetch an image allowed by localPatterns", async ({ page }) => {
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes");
expect(res.status()).toBe(200);
expect(res.headers()).toMatchObject({ "content-type": "image/webp" });
});

test("400 when fetching an image disallowed by localPatterns with wrong query parameter", async ({
page,
}) => {
const res = await page.request.get("/_next/image?url=/snipp/snipp?iscute=no");
expect(res.status()).toBe(400);
});

test("400 when fetching an image disallowed by localPatterns without query parameter", async ({
page,
}) => {
const res = await page.request.get("/_next/image?url=/snipp/snipp");
expect(res.status()).toBe(400);
});
});
});
6 changes: 6 additions & 0 deletions examples/playground14/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const nextConfig = {
pathname: "/u/248818",
},
],
localPatterns: [
{
pathname: "/snipp/**",
search: "?iscute=yes",
},
],
},
};

Expand Down
Binary file added examples/playground14/public/snipp/snipp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 31 additions & 3 deletions packages/cloudflare/src/cli/templates/images.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type RemotePattern = {
type RemotePattern = {
protocol?: "http" | "https";
hostname: string;
port?: string;
Expand All @@ -7,6 +7,12 @@ export type RemotePattern = {
search?: string;
};

type LocalPattern = {
// pathname is always set in the manifest
pathname: string;
search?: string;
};

/**
* Fetches an images.
*
Expand All @@ -22,16 +28,23 @@ export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) {
// Local
if (imageUrl.startsWith("/")) {
let pathname: string;
let url: URL;
try {
const url = new URL(imageUrl, "http://n");
// We only need pathname and search
url = new URL(imageUrl, "http://n");
pathname = decodeURIComponent(url.pathname);
} catch {
return getUrlErrorResponse();
}

if (/\/_next\/image($|\/)/.test(pathname)) {
return getUrlErrorResponse();
}

if (!__IMAGES_LOCAL_PATTERNS__.some((p: LocalPattern) => matchLocalPattern(p, url))) {
return getUrlErrorResponse();
}

return fetcher?.fetch(`http://assets.local${imageUrl}`);
}

Expand Down Expand Up @@ -83,6 +96,21 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
return true;
}

export function matchLocalPattern(pattern: LocalPattern, url: URL): boolean {
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-local-pattern.ts
if (pattern.search !== undefined) {
if (pattern.search !== url.search) {
return false;
}
}

if (!new RegExp(pattern.pathname).test(url.pathname)) {
return false;
}

return true;
}

/**
* @returns same error as Next.js when the url query parameter is not accepted.
*/
Expand All @@ -93,6 +121,6 @@ function getUrlErrorResponse() {
/* eslint-disable no-var */
declare global {
var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
var __IMAGES_LOCAL_PATTERNS__: unknown[];
var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
}
/* eslint-enable no-var */