Custom Cache-Control response header for `/_next/image`
See original GitHub issueBug report
Describe the bug
When requesting /_next/image(.*)
, I’m getting this Response header:
Cache-Control: public, max-age=60
And it’s fine… What’s not fine is that I’m getting the same exact Response headers even when I use custom headers in next.config.js
, i.e.:
module.exports = {
async headers() {
return [
{
// This works, and returns appropriate Response headers:
source: '/(.*).jpg',
headers: [
{
key: 'Cache-Control',
value:
'public, max-age=180, s-maxage=180, stale-while-revalidate=180',
},
],
},
{
// This doesn't work for 'Cache-Control' key (works for others though):
source: '/_next/image(.*)',
headers: [
{
key: 'Cache-Control',
// Instead of this value:
value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180',
// Cache-Control response header is `public, max-age=60` in production
// and `public, max-age=0, must-revalidate` in development
},
],
},
]
},
}
To Reproduce
Try this next.config.js
above, with an image that uses common <img>
tag like this:
<img src="/YOUR_IMAGE_IN_PUBLIC_FOLDER.jpg">
And another tag that uses the new next/image
component, with the same or whatever image url, for example:
<Image src="/YOUR_IMAGE_IN_PUBLIC_FOLDER.jpg">
And try requesting those images in the browser, and look into Devtools -> Network
tab, the Response headers, and see that for common <img>
tag it’s actually changed, whereas for new <Image>
component it is not.
Expected behavior
For /_next/image(.*)
urls, I expected to see Headers that I setup, i.e. Cache-Control: public, max-age=180, s-maxage=180, stale-while-revalidate=180
.
Screenshots
Common <img>
:
New <Image>
:
System information
- OS: not applicable / any
- Browser (if applies): not applicable / any
- Version of Next.js: 10.0.3
- Version of Node.js: 12.18.0
- Deployment: both
next start
on development machine, and actual Vercel deployment (screenshots demonstrate the case with local machine, but I’ve confirmed that the same is happening on a Vercel deployment)
Additional context
Originally I created an issue using “Feature request” template, and it got translated into “Idea discussion”: https://github.com/vercel/next.js/discussions/19896 (how to delete/close it pls? doesn’t seem to be possible), - initially I wasn’t sure if it’s even possible to setup custom headers, and how that supposed to work, though I just needed that functionality. But later, upon discovering that there’s such a functionality already, I figured out that actually /_next/image
overrides both custom headers in next.config.js
and headers property of Vercel config, i.e. it’s not just some idea of a feature that is “not implemented”, but it’s actual bug.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:99
- Comments:113 (22 by maintainers)
Top GitHub Comments
Intro
@timneutkens
There seem to be a lot of confusion about which
cache
this ticket is about.Just to prove my point, users have been posting about 4 types of
cache
:Status Code: 200 (from memory cache)
Status Code: 304 Not Modified
Most of the posts are about
#2
,#3
and#4
. Including interventions from NextJs team members.But the original post in this ticket is pretty clear the issue is about
#1
.Example with NextJs / Vercel
What I consider the happy path
I run NextJs
10.2.2
on NodeJs14.17.0
.I have a
public/img
folder where I put all my images.I use
next/image
to display images in my app.I build with
yarn build
which will run the default build command for NextJs.I don’t use any server-rendering, thus everything is statically rendered.
I installed the official Github plugin from Vercel so everytime I push on a branch, it gets deployed automatically on Vercel CDN.
Everything here is the default for an installation with NextJs and Vercel.
My
next.config.js
fileI’ve tried a lot of different combinations for
Cache-Control
, none of which worked, here are some of them:Running the
production
buildWhile running the
production
build, I open Chrome Dev Tools and look at theNetwork
tab.I click on an image entry to see its headers:
As you can see, no matter what I put in my
next.config.js
, theCache-Control
is never what I asked for, it’s always:We can see the
Etag
is working just fine because of theIf-None-Match
and the status code304 Not Modified
. But that would be case#2
:It’s impossible for me to make NextJs use case
#1
:Why case
#1
is desirable?With case
#1
, the browser cached the image and uses it directly without any network request if the cache isn’t expired.In the best case scenario where the cache is valid and available:
How case
#2
differs from case#1
?With case
#2
, the browser cached the image, but needs to make a network request to validate if the cached image is still valid before using it.In the best case scenario where the cache is valid and available:
#1
)#1
)Unfortunately, the best case scenario is relatively rare for case
#2
.If the unusally low cache time for the optimized image is expired (60 seconds only), NextJs needs to optimize the image again, which can push the whole latency upwards of 50 to 1200ms. This can be huge compared to the best case scenario, and it’s even worst compared to case
#1
.Now, if the image is NOT hosted on Vercel, say it’s hosted on an S3 buckets, the image needs to be downloaded from upstream (case
#3
). This can add another 40 to 100ms of latency.If your user has no network connection, the browser won’t make a network request to validate the
Etag
and use the cached image directly, which is a huge win.But if your user has a spotty network connection, the browser will make that costly network request and given the quality of the network, the latency is gonna be very bad.
All these are pretty common situations and these are all weakpoints for case
#2
. On the other hand, case#1
is working perfectly in these situations.Comparison with Cloudinary (image optimization service)
Cloudinary uses case
#1
in combination withEtag
and a fallback onlast-modified
.What it does:
Etag
andlast-modified
304 Not Modified
and renew the cache for another 30 daysYou only get to redownload the image if all 3 cache mechanism are busted: max-age, etag, last-modfied.
The only downside is that if you change your image for another and keep the same name, the users that already have the old image in cache might use that old one up to 30 days. On the other hand, I would argue you’re looking for trouble if you keep the same name and you wouldn’t get any sympathy from me on that one… Just change the name, problem solved instantly.
Conclusion
With all that said, I think you can better grasp why this ticket is asking for being able to overwrite the
Cache-Control
header so we can use case#1
.Best case scenario would be to keep the
Etag
so we can use it like Cloudinary does.I haven’t touched much on cases
#3
and#4
because they’re whole different cache types which is not the concern of this particular ticket.I’d just like to re-iterate I’m no expert on images. Maybe I didn’t understand something that was posted in this thread. Maybe I’m not aware of some constraints you have with the system’s inner workings. Maybe I’m making a really stupid mistake on my side and that’s why I can’t make it work. These are all plausible possibilities.
But on the off-chance that I actually nailed it, I hope my post will help you understand what this ticket was aiming for before all the posts for other
cache
types made it confusing.We fell into this issue today too. According to the docs the upstream cache control header should be used: https://nextjs.org/docs/basic-features/image-optimization#caching
Looking at the code it hard codes the
max-age=0
into the response: https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/image-optimizer.ts#L310This is not usable for us as our page contains lots of images. If they are not cached our users waste lots of bandwidth. Is there a timeline when this issue will be fixed? Thanks!