Repository: vercel/next.js
Pull request: fix: add backwards compat for middleware → proxy field renames

### What?

These config keys were recently soft-renamed with forward compatibility on the _resolved_ config object:

- `experimental.proxyPrefetch` → `experimental.middlewarePrefetch`
- `experimental.externalProxyRewritesResolve` → `experimental.externalMiddlewareRewritesResolve`
- `skipProxyUrlNormalize` → `skipMiddlewareUrlNormalize`

But there was no backwards compatibility on the resolved, which is (arguably) an undocumented breaking change.

### Why?

This avoids introducing a last-minute breaking change for providers and other integrations that read the the `.next/` build output files (which is necessary until we all move to the upcoming [Adapter API](https://github.com/vercel/next.js/discussions/77740).

### How?

When a new `proxy` config key is set but the old `middleware` name is not, copy the value to the old name on the *resolved* config object.

This also adds test coverage for both the existing forward compat (old→new) and the new backwards compat (new→old).

Changed files:
filename: packages/next/src/server/config.test.ts
patch: @@ -134,4 +134,77 @@ describe('loadConfig', () => {
       )
     })
   })
+
+  describe('middleware to proxy config key rename backward/forward compatibility', () => {
+    it('should copy `skipMiddlewareUrlNormalize value` to `skipProxyUrlNormalize`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          skipMiddlewareUrlNormalize: true,
+        },
+      })
+
+      expect(result.skipProxyUrlNormalize).toBe(true)
+    })
+
+    it('should copy `experimental.middlewarePrefetch` to `experimental.proxyPrefetch`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          experimental: {
+            middlewarePrefetch: 'strict',
+          },
+        },
+      })
+
+      expect(result.experimental.proxyPrefetch).toBe('strict')
+    })
+
+    it('should copy `experimental.externalMiddlewareRewritesResolve` to `experimental.externalProxyRewritesResolve`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          experimental: {
+            externalMiddlewareRewritesResolve: true,
+          },
+        },
+      })
+
+      expect(result.experimental.externalProxyRewritesResolve).toBe(true)
+    })
+
+    it('should copy `skipProxyUrlNormalize` to `skipMiddlewareUrlNormalize`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          skipProxyUrlNormalize: true,
+        },
+      })
+
+      expect(result.skipMiddlewareUrlNormalize).toBe(true)
+      expect(result.skipProxyUrlNormalize).toBe(true)
+    })
+
+    it('should copy `experimental.proxyPrefetch` to `experimental.middlewarePrefetch`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          experimental: {
+            proxyPrefetch: 'strict',
+          },
+        },
+      })
+
+      expect(result.experimental.middlewarePrefetch).toBe('strict')
+      expect(result.experimental.proxyPrefetch).toBe('strict')
+    })
+
+    it('should copy `experimental.externalProxyRewritesResolve` to `experimental.externalMiddlewareRewritesResolve`', async () => {
+      const result = await loadConfig(PHASE_PRODUCTION_BUILD, __dirname, {
+        customConfig: {
+          experimental: {
+            externalProxyRewritesResolve: true,
+          },
+        },
+      })
+
+      expect(result.experimental.externalMiddlewareRewritesResolve).toBe(true)
+      expect(result.experimental.externalProxyRewritesResolve).toBe(true)
+    })
+  })
 })

filename: packages/next/src/server/config.ts
patch: @@ -836,6 +836,28 @@ function assignDefaultsAndValidate(
   ) {
     result.skipProxyUrlNormalize = userConfig.skipMiddlewareUrlNormalize
   }
+  // Inverse case: when new name is set but not the old name, copy the value to the old name
+  // to avoid breaking change on resolved config object written to `.next/`
+  if (
+    userConfig.experimental?.proxyPrefetch !== undefined &&
+    userConfig.experimental?.middlewarePrefetch === undefined
+  ) {
+    result.experimental.middlewarePrefetch =
+      userConfig.experimental.proxyPrefetch
+  }
+  if (
+    userConfig.experimental?.externalProxyRewritesResolve !== undefined &&
+    userConfig.experimental?.externalMiddlewareRewritesResolve === undefined
+  ) {
+    result.experimental.externalMiddlewareRewritesResolve =
+      userConfig.experimental.externalProxyRewritesResolve
+  }
+  if (
+    userConfig.skipProxyUrlNormalize !== undefined &&
+    userConfig.skipMiddlewareUrlNormalize === undefined
+  ) {
+    result.skipMiddlewareUrlNormalize = userConfig.skipProxyUrlNormalize
+  }
 
   // Normalize & validate experimental.proxyClientMaxBodySize
   if (typeof result.experimental?.proxyClientMaxBodySize !== 'undefined') {