Repository: vercel/next.js
Pull request: Support `Uint8Array` bodies when generating cache keys for POST requests

When opting into caching a POST request with the `revalidate` option, or when the Server Components HMR Cache implicitly caches a POST request, the body of the request is now correctly handled if it is a `Uint8Array`. Previously, we logged `Failed to generate cache key for ...` and the cache was bypassed.

closes NAR-180

Changed files:
filename: packages/next/src/server/lib/incremental-cache/index.ts
patch: @@ -293,8 +293,12 @@ export class IncrementalCache implements IncrementalCacheType {
     const decoder = new TextDecoder()
 
     if (init.body) {
-      // handle ReadableStream body
-      if (typeof (init.body as any).getReader === 'function') {
+      // handle Uint8Array body
+      if (init.body instanceof Uint8Array) {
+        bodyChunks.push(decoder.decode(init.body))
+        ;(init as any)._ogBody = init.body
+      } // handle ReadableStream body
+      else if (typeof (init.body as any).getReader === 'function') {
         const readableBody = init.body as ReadableStream<Uint8Array | string>
 
         const chunks: Uint8Array[] = []

filename: test/e2e/app-dir/app-static/app-static.test.ts
patch: @@ -907,8 +907,6 @@ describe('app-dir static/dynamic handling', () => {
          "variable-revalidate/encoding.rsc",
          "variable-revalidate/headers-instance.html",
          "variable-revalidate/headers-instance.rsc",
-         "variable-revalidate/post-method.html",
-         "variable-revalidate/post-method.rsc",
          "variable-revalidate/revalidate-3.html",
          "variable-revalidate/revalidate-3.rsc",
          "variable-revalidate/revalidate-360-isr.html",
@@ -2278,31 +2276,6 @@ describe('app-dir static/dynamic handling', () => {
            "initialRevalidateSeconds": 10,
            "srcRoute": "/variable-revalidate/headers-instance",
          },
-         "/variable-revalidate/post-method": {
-           "allowHeader": [
-             "host",
-             "x-matched-path",
-             "x-prerender-revalidate",
-             "x-prerender-revalidate-if-generated",
-             "x-next-revalidated-tags",
-             "x-next-revalidate-tag-token",
-           ],
-           "dataRoute": "/variable-revalidate/post-method.rsc",
-           "experimentalBypassFor": [
-             {
-               "key": "Next-Action",
-               "type": "header",
-             },
-             {
-               "key": "content-type",
-               "type": "header",
-               "value": "multipart/form-data;.*",
-             },
-           ],
-           "initialExpireSeconds": 31536000,
-           "initialRevalidateSeconds": 10,
-           "srcRoute": "/variable-revalidate/post-method",
-         },
          "/variable-revalidate/revalidate-3": {
            "allowHeader": [
              "host",
@@ -3451,10 +3424,12 @@ describe('app-dir static/dynamic handling', () => {
       const dataBody2 = $('#data-body2').text()
       const dataBody3 = $('#data-body3').text()
       const dataBody4 = $('#data-body4').text()
+      const dataBody5 = $('#data-body5').text()
 
       expect(dataBody1).not.toBe(dataBody2)
       expect(dataBody2).not.toBe(dataBody3)
       expect(dataBody3).not.toBe(dataBody4)
+      expect(dataBody4).not.toBe(dataBody5)
 
       const res2 = await fetchViaHTTP(
         next.url,
@@ -3469,6 +3444,8 @@ describe('app-dir static/dynamic handling', () => {
       expect($2('#data-body1').text()).toBe(dataBody1)
       expect($2('#data-body2').text()).toBe(dataBody2)
       expect($2('#data-body3').text()).toBe(dataBody3)
+      expect($2('#data-body4').text()).toBe(dataBody4)
+      expect($2('#data-body5').text()).toBe(dataBody5)
       return 'success'
     }, 'success')
   })

filename: test/e2e/app-dir/app-static/app/variable-revalidate/post-method/page.js
patch: @@ -1,5 +1,7 @@
 import { fetchRetry } from '../../../lib/fetch-retry'
 
+export const dynamic = 'force-dynamic'
+
 export default async function Page() {
   const data = await fetchRetry(
     'https://next-data-api-endpoint.vercel.app/api/random',
@@ -55,6 +57,20 @@ export default async function Page() {
     {
       method: 'POST',
       body: new URLSearchParams('myParam=myValue&myParam=diffValue'),
+      next: {
+        revalidate: 30,
+      },
+    }
+  ).then((res) => res.text())
+
+  const dataWithBody5 = await fetchRetry(
+    'https://next-data-api-endpoint.vercel.app/api/random',
+    {
+      method: 'POST',
+      body: new TextEncoder().encode(JSON.stringify({ hi: 'there' })),
+      next: {
+        revalidate: 30,
+      },
     }
   ).then((res) => res.text())
 
@@ -66,6 +82,7 @@ export default async function Page() {
       <p id="data-body2">{dataWithBody2}</p>
       <p id="data-body3">{dataWithBody3}</p>
       <p id="data-body4">{dataWithBody4}</p>
+      <p id="data-body5">{dataWithBody5}</p>
     </>
   )
 }