Update and explanation:
For caching this appears to be the documented behavior: see vercel/next.js#62875
That still leaves the question of why request memoization (as distinct from
caching) doesn't stop the request being made twice for one page render. Turns
out that this is because request memoization doesn't occur in dev mode. So
you'll see different behavior if you follow the instructions below but use
npm run build && npm start
instead of npm run dev
.
A NextJS app for reproducing a caching issue with fetch
. Runs on
[email protected]
.
If a page.jsx
file makes a call to cookies()
from "next/headers"
,
subsequent calls to fetch
in the page rendering code do not use the fetch
cache unless the cache: 'force-cache'
option is explicitly passed or the
next: { revalidate: n }
option is passed for n > 0
.
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 21.2.0: Sun Nov 28 20:28:41 PST 2021; root:xnu-8019.61.5~1/RELEASE_ARM64_T6000
Binaries:
Node: 21.6.2
npm: 10.2.4
Relevant Packages:
next: 14.1.2-canary.3
react: 18.2.0
react-dom: 18.2.0
Next.js Config:
output: N/A
- Docs suggest that
cache: 'force-cache'
is the default forfetch
(see here), but section 3 in the repro instructions below shows thatfetch
can behave differently when this option is set. - If a page accesses cookies during SSR, it seems counterintuitive that this
should turn off caching for any
fetch
calls that its rendering code makes to any external APIs. - The comment above this LOC seems to suggest that some unintended behavior might have sneaked into the logic at some point. (I have observed the same issue on 14.0 and 14.1 releases, for context.)
The key logic is
Start the app on localhost:3000:
npm i
npm run dev
The app has a /test
page that makes two requests via fetch
to
http://localhost:8080/test
.
Start an HTTP server on localhost:8080 that echos any requests received:
node ./echo.js
This server stands in for an external API called by the page rendering code.
curl http://localhost:3000/test
On the first run of curl
, with an empty cache, console 2 should show one
GET request to /foo
(indicating that the second request was cached).
On any additional run of curl http://localhost:3000/test
, no requests should
be made to /foo
(again indicating the use of the cache).
Edit app/test/page.jsx
and uncomment the call to cookies()
.
Now when curl http://localhost:3000/test
is run, console 2 should show two
GET requests being made to /foo
. This indicates that the cache is not being
used.
With the call to cookies()
still uncommented, replace the call to fetch
in app/test/page.jsx
with
await fetch("http://localhost:8080/foo", { method: "GET", cache: 'force-cache' })
Now in console 3 run curl http://localhost:3000/test
.
The cache is used (i.e. no GET requests are made to /foo
), even with the call
to cookies()
in page.jsx
.
With the call to cookies()
still uncommented, replace the call to fetch
in app/test/page.jsx
with
await fetch("http://localhost:8080/foo", { method: "GET", next: { revalidate: 999} })
Now in console 3 run curl http://localhost:3000/test
.
The cache is used (i.e. no GET requests are made to /foo
), even with the call
to cookies()
in page.jsx
.
It can be useful to clear the fetch cache when testing behavior:
rm -rf .next/cache/fetch-cache/