From 6ebb9dcdde396a761a6a52c17d8c2ad56792d34e Mon Sep 17 00:00:00 2001
From: Chris Shaw <me@cshaw.xyz>
Date: Mon, 26 May 2025 14:37:30 +1000
Subject: [PATCH] feat: allow next middleware to use edge cache

---
 src/build/functions/edge.ts                   |  5 ++-
 .../middleware-edge-cache/app/layout.js       | 12 ++++++
 .../middleware-edge-cache/app/page.js         |  7 ++++
 .../app/test/cached/page.js                   |  7 ++++
 .../app/test/next/page.js                     | 12 ++++++
 .../app/test/uncached/page.js                 |  7 ++++
 .../middleware-edge-cache/middleware.ts       | 38 +++++++++++++++++++
 .../middleware-edge-cache/next.config.js      |  9 +++++
 .../middleware-edge-cache/package.json        | 15 ++++++++
 tests/integration/edge-handler.test.ts        | 36 ++++++++++++++++++
 10 files changed, 147 insertions(+), 1 deletion(-)
 create mode 100644 tests/fixtures/middleware-edge-cache/app/layout.js
 create mode 100644 tests/fixtures/middleware-edge-cache/app/page.js
 create mode 100644 tests/fixtures/middleware-edge-cache/app/test/cached/page.js
 create mode 100644 tests/fixtures/middleware-edge-cache/app/test/next/page.js
 create mode 100644 tests/fixtures/middleware-edge-cache/app/test/uncached/page.js
 create mode 100644 tests/fixtures/middleware-edge-cache/middleware.ts
 create mode 100644 tests/fixtures/middleware-edge-cache/next.config.js
 create mode 100644 tests/fixtures/middleware-edge-cache/package.json

diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index 1cf4554a0f..e66d5d0960 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -172,7 +172,10 @@ const buildHandlerDefinition = (
   const functionName = name.endsWith('middleware')
     ? 'Next.js Middleware Handler'
     : `Next.js Edge Handler: ${page}`
-  const cache = name.endsWith('middleware') ? undefined : ('manual' as const)
+  const cache =
+    !process.env.NEXT_MIDDLEWARE_CACHE && name.endsWith('middleware')
+      ? undefined
+      : ('manual' as const)
   const generator = `${ctx.pluginName}@${ctx.pluginVersion}`
 
   return augmentMatchers(matchers, ctx).map((matcher) => ({
diff --git a/tests/fixtures/middleware-edge-cache/app/layout.js b/tests/fixtures/middleware-edge-cache/app/layout.js
new file mode 100644
index 0000000000..6565e7bafd
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/app/layout.js
@@ -0,0 +1,12 @@
+export const metadata = {
+  title: 'Simple Next App',
+  description: 'Description for Simple Next App',
+}
+
+export default function RootLayout({ children }) {
+  return (
+    <html lang="en">
+      <body>{children}</body>
+    </html>
+  )
+}
diff --git a/tests/fixtures/middleware-edge-cache/app/page.js b/tests/fixtures/middleware-edge-cache/app/page.js
new file mode 100644
index 0000000000..1a9fe06903
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/app/page.js
@@ -0,0 +1,7 @@
+export default function Home() {
+  return (
+    <main>
+      <h1>Home</h1>
+    </main>
+  )
+}
diff --git a/tests/fixtures/middleware-edge-cache/app/test/cached/page.js b/tests/fixtures/middleware-edge-cache/app/test/cached/page.js
new file mode 100644
index 0000000000..f4bf7ab134
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/app/test/cached/page.js
@@ -0,0 +1,7 @@
+export default function EdgeCached() {
+  return (
+    <main>
+      <h1>If middleware works, we shoudn't get here</h1>
+    </main>
+  )
+}
diff --git a/tests/fixtures/middleware-edge-cache/app/test/next/page.js b/tests/fixtures/middleware-edge-cache/app/test/next/page.js
new file mode 100644
index 0000000000..0908c69938
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/app/test/next/page.js
@@ -0,0 +1,12 @@
+import { headers } from 'next/headers'
+
+export default function Page() {
+  const headersList = headers()
+  const message = headersList.get('x-hello-from-middleware-req')
+
+  return (
+    <main>
+      <h1>Message from middleware: {message}</h1>
+    </main>
+  )
+}
diff --git a/tests/fixtures/middleware-edge-cache/app/test/uncached/page.js b/tests/fixtures/middleware-edge-cache/app/test/uncached/page.js
new file mode 100644
index 0000000000..a098b8cdde
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/app/test/uncached/page.js
@@ -0,0 +1,7 @@
+export default function EdgeUncached() {
+  return (
+    <main>
+      <h1>If middleware works, we shoudn't get here</h1>
+    </main>
+  )
+}
diff --git a/tests/fixtures/middleware-edge-cache/middleware.ts b/tests/fixtures/middleware-edge-cache/middleware.ts
new file mode 100644
index 0000000000..041475f846
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/middleware.ts
@@ -0,0 +1,38 @@
+import type { NextRequest } from 'next/server'
+import { NextResponse } from 'next/server'
+
+export function middleware(request: NextRequest) {
+  const response = getResponse(request)
+
+  response.headers.append('Deno' in globalThis ? 'x-deno' : 'x-node', Date.now().toString())
+  response.headers.set('x-hello-from-middleware-res', 'hello')
+
+  return response
+}
+
+const getResponse = (request: NextRequest) => {
+  const requestHeaders = new Headers(request.headers)
+
+  requestHeaders.set('x-hello-from-middleware-req', 'hello')
+
+  if (request.nextUrl.pathname === '/test/cached') {
+    return NextResponse.next({
+      request: {
+        headers: new Headers({
+          ...requestHeaders,
+          'netlify-cdn-cache-control': 's-maxage=31536000, durable',
+        }),
+      },
+    })
+  }
+
+  if (request.nextUrl.pathname === '/test/uncached') {
+    return NextResponse.next({
+      request: {
+        headers: requestHeaders,
+      },
+    })
+  }
+
+  return NextResponse.json({ error: 'Error' }, { status: 500 })
+}
diff --git a/tests/fixtures/middleware-edge-cache/next.config.js b/tests/fixtures/middleware-edge-cache/next.config.js
new file mode 100644
index 0000000000..9d94510be1
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/next.config.js
@@ -0,0 +1,9 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  output: 'standalone',
+  eslint: {
+    ignoreDuringBuilds: true,
+  },
+}
+
+module.exports = nextConfig
diff --git a/tests/fixtures/middleware-edge-cache/package.json b/tests/fixtures/middleware-edge-cache/package.json
new file mode 100644
index 0000000000..c126212eb8
--- /dev/null
+++ b/tests/fixtures/middleware-edge-cache/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "middleware-edge-cache",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "postinstall": "next build",
+    "dev": "next dev",
+    "build": "next build"
+  },
+  "dependencies": {
+    "next": "latest",
+    "react": "18.2.0",
+    "react-dom": "18.2.0"
+  }
+}
diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts
index 825ed6fac1..9b93fd5761 100644
--- a/tests/integration/edge-handler.test.ts
+++ b/tests/integration/edge-handler.test.ts
@@ -625,4 +625,40 @@ describe('page router', () => {
     expect(bodyFr.nextUrlPathname).toBe('/json')
     expect(bodyFr.nextUrlLocale).toBe('fr')
   })
+
+  test<FixtureTestContext>('should use edge caching', async (ctx) => {
+    vi.stubEnv('NEXT_MIDDLEWARE_CACHE', 'true')
+
+    await createFixture('middleware-edge-cache', ctx)
+    await runPlugin(ctx)
+    const origin = await LocalServer.run(async (req, res) => {
+      res.write(
+        JSON.stringify({
+          url: req.url,
+          headers: req.headers,
+        }),
+      )
+      res.end()
+    })
+    ctx.cleanup?.push(() => origin.stop())
+
+    const response = await invokeEdgeFunction(ctx, {
+      functions: ['___netlify-edge-handler-middleware'],
+      origin,
+      url: `/test/cached`,
+    })
+    expect(response.status).toBe(200)
+
+    expect(response.headers.has('netlify-cdn-cache-control')).toBeTruthy()
+    expect(response.headers.get('netlify-cdn-cache-control')).toBe('s-maxage=31536000, durable')
+
+    const responseUncached = await invokeEdgeFunction(ctx, {
+      functions: ['___netlify-edge-handler-middleware'],
+      origin,
+      url: `/test/uncached`,
+    })
+    expect(responseUncached.status).toBe(200)
+
+    expect(responseUncached.headers.has('netlify-cdn-cache-control')).toBeFalsy()
+  })
 })