diff --git a/examples/app-pages-router/app/image-optimization/page.tsx b/examples/app-pages-router/app/image-optimization/page.tsx
new file mode 100644
index 000000000..f43655af4
--- /dev/null
+++ b/examples/app-pages-router/app/image-optimization/page.tsx
@@ -0,0 +1,14 @@
+import Image from "next/image";
+
+export default function ImageOptimization() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/app-pages-router/app/page.tsx b/examples/app-pages-router/app/page.tsx
index 0fd02970e..1b32a5c3d 100644
--- a/examples/app-pages-router/app/page.tsx
+++ b/examples/app-pages-router/app/page.tsx
@@ -33,6 +33,9 @@ export default function Home() {
+
Pages Router
diff --git a/examples/app-pages-router/public/static/corporate_holiday_card.jpg b/examples/app-pages-router/public/static/corporate_holiday_card.jpg
new file mode 100644
index 000000000..0df96ae23
Binary files /dev/null and b/examples/app-pages-router/public/static/corporate_holiday_card.jpg differ
diff --git a/examples/app-router/app/image-optimization/page.tsx b/examples/app-router/app/image-optimization/page.tsx
new file mode 100644
index 000000000..baba473bc
--- /dev/null
+++ b/examples/app-router/app/image-optimization/page.tsx
@@ -0,0 +1,14 @@
+import Image from "next/image";
+
+export default function ImageOptimization() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/app-router/app/page.tsx b/examples/app-router/app/page.tsx
index 2f347d3ca..e07d65a31 100644
--- a/examples/app-router/app/page.tsx
+++ b/examples/app-router/app/page.tsx
@@ -47,6 +47,9 @@ export default function Home() {
+
>
);
diff --git a/examples/app-router/next.config.js b/examples/app-router/next.config.js
index 447979148..ce0565a83 100644
--- a/examples/app-router/next.config.js
+++ b/examples/app-router/next.config.js
@@ -8,6 +8,14 @@ const nextConfig = {
experimental: {
serverActions: true,
},
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "open-next.js.org",
+ },
+ ],
+ },
redirects: () => {
return [
{
diff --git a/examples/app-router/public/static/corporate_holiday_card.jpg b/examples/app-router/public/static/corporate_holiday_card.jpg
new file mode 100644
index 000000000..0df96ae23
Binary files /dev/null and b/examples/app-router/public/static/corporate_holiday_card.jpg differ
diff --git a/packages/open-next/CHANGELOG.md b/packages/open-next/CHANGELOG.md
index 40998f46b..e3fb0e95c 100644
--- a/packages/open-next/CHANGELOG.md
+++ b/packages/open-next/CHANGELOG.md
@@ -1,5 +1,18 @@
# open-next
+## 2.3.7
+
+### Patch Changes
+
+- 3235392: fix: prevent duplication of location header
+- af2d3ce: Fix Image Optimization Support for Next@14.1.1
+
+## 2.3.6
+
+### Patch Changes
+
+- f9b90b6: Security fix: sharp@0.33.2
+
## 2.3.5
### Patch Changes
diff --git a/packages/open-next/src/adapters/image-optimization-adapter.ts b/packages/open-next/src/adapters/image-optimization-adapter.ts
index f27ef0f42..f8a0b1434 100644
--- a/packages/open-next/src/adapters/image-optimization-adapter.ts
+++ b/packages/open-next/src/adapters/image-optimization-adapter.ts
@@ -8,12 +8,10 @@ import path from "node:path";
import { Writable } from "node:stream";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
-import type { APIGatewayProxyEventHeaders } from "aws-lambda";
import { loadConfig } from "config/util.js";
// @ts-ignore
import { defaultConfig } from "next/dist/server/config-shared";
import {
- imageOptimizer,
ImageOptimizerCache,
// @ts-ignore
} from "next/dist/server/image-optimizer";
@@ -23,6 +21,7 @@ import { ImageLoader, InternalEvent, InternalResult } from "types/open-next.js";
import { createGenericHandler } from "../core/createGenericHandler.js";
import { awsLogger, debug, error } from "./logger.js";
+import { optimizeImage } from "./plugins/image-optimization/image-optimization.js";
import { setNodeEnv } from "./util.js";
// Expected environment variables
@@ -69,7 +68,12 @@ export async function defaultHandler(
headers,
queryString === null ? undefined : queryString,
);
- const result = await optimizeImage(headers, imageParams);
+ const result = await optimizeImage(
+ headers,
+ imageParams,
+ nextConfig,
+ downloadHandler,
+ );
return buildSuccessResponse(result);
} catch (e: any) {
@@ -115,23 +119,6 @@ function validateImageParams(
return imageParams;
}
-async function optimizeImage(
- headers: APIGatewayProxyEventHeaders,
- imageParams: any,
-) {
- const result = await imageOptimizer(
- // @ts-ignore
- { headers },
- {}, // res object is not necessary as it's not actually used.
- imageParams,
- nextConfig,
- false, // not in dev mode
- downloadHandler,
- );
- debug("optimized result", result);
- return result;
-}
-
function buildSuccessResponse(result: any): InternalResult {
return {
type: "core",
diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts
new file mode 100644
index 000000000..4b948be4d
--- /dev/null
+++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts
@@ -0,0 +1,51 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
+
+import type { APIGatewayProxyEventHeaders } from "aws-lambda";
+import type { NextConfig } from "next/dist/server/config-shared";
+//#override imports
+import {
+ // @ts-ignore
+ fetchExternalImage,
+ // @ts-ignore
+ fetchInternalImage,
+ imageOptimizer,
+} from "next/dist/server/image-optimizer";
+//#endOverride
+import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
+
+import { debug } from "../../logger.js";
+
+//#override optimizeImage
+export async function optimizeImage(
+ headers: APIGatewayProxyEventHeaders,
+ imageParams: any,
+ nextConfig: NextConfig,
+ handleRequest: (
+ newReq: IncomingMessage,
+ newRes: ServerResponse,
+ newParsedUrl?: NextUrlWithParsedQuery,
+ ) => Promise,
+) {
+ const { isAbsolute, href } = imageParams;
+
+ const imageUpstream = isAbsolute
+ ? await fetchExternalImage(href)
+ : await fetchInternalImage(
+ href,
+ // @ts-ignore
+ { headers },
+ {}, // res object is not necessary as it's not actually used.
+ handleRequest,
+ );
+
+ // @ts-ignore
+ const result = await imageOptimizer(
+ imageUpstream,
+ imageParams,
+ nextConfig,
+ false, // not in dev mode
+ );
+ debug("optimized result", result);
+ return result;
+}
+//#endOverride
diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts
new file mode 100644
index 000000000..5a9fc75ea
--- /dev/null
+++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts
@@ -0,0 +1,35 @@
+import { IncomingMessage, ServerResponse } from "node:http";
+
+import { APIGatewayProxyEventHeaders } from "aws-lambda";
+import { NextConfig } from "next/dist/server/config-shared";
+//#override imports
+import { imageOptimizer } from "next/dist/server/image-optimizer";
+//#endOverride
+import { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
+
+import { debug } from "../../logger.js";
+
+//#override optimizeImage
+export async function optimizeImage(
+ headers: APIGatewayProxyEventHeaders,
+ imageParams: any,
+ nextConfig: NextConfig,
+ handleRequest: (
+ newReq: IncomingMessage,
+ newRes: ServerResponse,
+ newParsedUrl: NextUrlWithParsedQuery,
+ ) => Promise,
+) {
+ const result = await imageOptimizer(
+ // @ts-ignore
+ { headers },
+ {}, // res object is not necessary as it's not actually used.
+ imageParams,
+ nextConfig,
+ false, // not in dev mode
+ handleRequest,
+ );
+ debug("optimized result", result);
+ return result;
+}
+//#endOverride
diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts
index b1642a863..7b19e6a10 100755
--- a/packages/open-next/src/build.ts
+++ b/packages/open-next/src/build.ts
@@ -1,5 +1,6 @@
import cp from "node:child_process";
import fs, { readFileSync } from "node:fs";
+import { createRequire as topLevelCreateRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import url from "node:url";
@@ -13,6 +14,7 @@ import { buildEdgeBundle } from "./build/edge/createEdgeBundle.js";
import { generateOutput } from "./build/generateOutput.js";
import {
BuildOptions,
+ compareSemver,
copyOpenNextConfig,
esbuildAsync,
esbuildSync,
@@ -24,9 +26,11 @@ import {
} from "./build/helper.js";
import { validateConfig } from "./build/validateConfig.js";
import logger from "./logger.js";
+import { openNextReplacementPlugin } from "./plugins/replacement.js";
import { openNextResolvePlugin } from "./plugins/resolve.js";
import { OpenNextConfig } from "./types/open-next.js";
+const require = topLevelCreateRequire(import.meta.url);
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
let options: BuildOptions;
let config: OpenNextConfig;
@@ -74,9 +78,10 @@ export async function build(openNextConfigPath?: string) {
createStaticAssets();
await createCacheAssets(monorepoRoot);
+
await createServerBundle(config, options);
await createRevalidationBundle();
- createImageOptimizationBundle();
+ await createImageOptimizationBundle();
await createWarmerBundle();
await generateOutput(options.appBuildOutputPath, config);
logger.info("OpenNext build complete.");
@@ -314,7 +319,7 @@ async function createRevalidationBundle() {
);
}
-function createImageOptimizationBundle() {
+async function createImageOptimizationBundle() {
logger.info(`Bundling image optimization function...`);
const { appPath, appBuildOutputPath, outputDir } = options;
@@ -326,17 +331,33 @@ function createImageOptimizationBundle() {
// Copy open-next.config.mjs into the bundle
copyOpenNextConfig(options.tempDir, outputPath);
+ const plugins =
+ compareSemver(options.nextVersion, "14.1.1") >= 0
+ ? [
+ openNextReplacementPlugin({
+ name: "opennext-14.1.1-image-optimization",
+ target: /plugins\/image-optimization\/image-optimization\.js/g,
+ replacements: [
+ require.resolve(
+ "./adapters/plugins/image-optimization/image-optimization.replacement.js",
+ ),
+ ],
+ }),
+ ]
+ : undefined;
+
// Build Lambda code (1st pass)
// note: bundle in OpenNext package b/c the adapter relies on the
// "@aws-sdk/client-s3" package which is not a dependency in user's
// Next.js app.
- esbuildSync(
+ await esbuildAsync(
{
entryPoints: [
path.join(__dirname, "adapters", "image-optimization-adapter.js"),
],
external: ["sharp", "next"],
outfile: path.join(outputPath, "index.mjs"),
+ plugins,
},
options,
);
@@ -375,7 +396,7 @@ function createImageOptimizationBundle() {
// For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda
const nodeOutputPath = path.resolve(outputPath);
- const sharpVersion = process.env.SHARP_VERSION ?? "0.32.5";
+ const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6";
//check if we are running in Windows environment then set env variables accordingly.
try {
diff --git a/packages/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts
index b7dd0e0f6..2de20cfcc 100644
--- a/packages/open-next/src/core/routing/middleware.ts
+++ b/packages/open-next/src/core/routing/middleware.ts
@@ -122,21 +122,19 @@ export async function handleMiddleware(
// If the middleware returned a Redirect, we set the `Location` header with
// the redirected url and end the response.
if (statusCode >= 300 && statusCode < 400) {
- const location = result.headers
- .get("location")
- ?.replace(
- "http://localhost:3000",
- `https://${internalEvent.headers.host}`,
- );
+ resHeaders.location =
+ responseHeaders
+ .get("location")
+ ?.replace(
+ "http://localhost:3000",
+ `https://${internalEvent.headers.host}`,
+ ) ?? resHeaders.location;
// res.setHeader("Location", location);
return {
body: "",
type: internalEvent.type,
statusCode: statusCode,
- headers: {
- ...resHeaders,
- Location: location ?? "",
- },
+ headers: resHeaders,
isBase64Encoded: false,
};
}
diff --git a/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts b/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts
new file mode 100644
index 000000000..c26cfe6ec
--- /dev/null
+++ b/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from "@playwright/test";
+
+test("Image Optimization", async ({ page }) => {
+ await page.goto("/");
+
+ const imageResponsePromise = page.waitForResponse(
+ /corporate_holiday_card.jpg/,
+ );
+ await page.locator('[href="/image-optimization"]').click();
+ const imageResponse = await imageResponsePromise;
+
+ await page.waitForURL("/image-optimization");
+
+ const imageContentType = imageResponse.headers()["content-type"];
+ expect(imageContentType).toBe("image/webp");
+
+ let el = page.locator("img");
+ await expect(el).toHaveJSProperty("complete", true);
+ await expect(el).not.toHaveJSProperty("naturalWidth", 0);
+});
diff --git a/packages/tests-e2e/tests/appRouter/image-optimization.test.ts b/packages/tests-e2e/tests/appRouter/image-optimization.test.ts
new file mode 100644
index 000000000..66eb64ad0
--- /dev/null
+++ b/packages/tests-e2e/tests/appRouter/image-optimization.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from "@playwright/test";
+
+test("Image Optimization", async ({ page }) => {
+ await page.goto("/");
+
+ const imageResponsePromise = page.waitForResponse(
+ /https%3A%2F%2Fopen-next.js.org%2Farchitecture.png/,
+ );
+ await page.locator('[href="/image-optimization"]').click();
+ const imageResponse = await imageResponsePromise;
+
+ await page.waitForURL("/image-optimization");
+
+ const imageContentType = imageResponse.headers()["content-type"];
+ expect(imageContentType).toBe("image/webp");
+
+ let el = page.locator("img");
+ await expect(el).toHaveJSProperty("complete", true);
+ await expect(el).not.toHaveJSProperty("naturalWidth", 0);
+});
diff --git a/packages/tests-unit/CHANGELOG.md b/packages/tests-unit/CHANGELOG.md
index 090bd5dd5..c95583030 100644
--- a/packages/tests-unit/CHANGELOG.md
+++ b/packages/tests-unit/CHANGELOG.md
@@ -4,6 +4,21 @@
### Patch Changes
+- Updated dependencies [3235392]
+- Updated dependencies [af2d3ce]
+ - open-next@2.3.7
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [f9b90b6]
+ - open-next@2.3.6
+
+## null
+
+### Patch Changes
+
- Updated dependencies [b9eefca]
- Updated dependencies [c80f1be]
- Updated dependencies [186e28f]