nextjs - ✅(Solved) Fix HEAD requests for Next.js image return 400 Bad Request when image is not yet cached [1 pull requests, 11 comments, 7 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#84129Fetched 2026-04-08 02:20:30
View on GitHub
Comments
11
Participants
7
Timeline
34
Reactions
9
Author
Assignees
Timeline (top)
commented ×11subscribed ×11labeled ×3mentioned ×2

When making a HEAD request to an image served via the Next.js Image Optimizer (/_next/image) before the image is cached, the server returns 400 Bad Request.

  • On Next.js 15.3.x:
    • A first HEAD request poisoned the cache by storing an empty file, causing subsequent GET requests to return an empty response.
  • On Next.js 15.5.x:
    • The cache poisoning seems fixed (no empty files are written).
    • However, the first uncached HEAD request still fails with 400 Bad Request.
    • Only after a GET request succeeds and the image is cached, subsequent HEAD requests return 200 OK.

This behavior breaks CDNs/proxies that issue HEAD requests for validation or preflight, since they receive a 400 instead of headers describing the resource.

Error Message

  • First uncached HEAD request → 400 Bad Request with error logs (received null).

Root Cause

When making a HEAD request to an image served via the Next.js Image Optimizer (/_next/image) before the image is cached, the server returns 400 Bad Request.

  • On Next.js 15.3.x:
    • A first HEAD request poisoned the cache by storing an empty file, causing subsequent GET requests to return an empty response.
  • On Next.js 15.5.x:
    • The cache poisoning seems fixed (no empty files are written).
    • However, the first uncached HEAD request still fails with 400 Bad Request.
    • Only after a GET request succeeds and the image is cached, subsequent HEAD requests return 200 OK.

This behavior breaks CDNs/proxies that issue HEAD requests for validation or preflight, since they receive a 400 instead of headers describing the resource.

Fix Action

Fixed

PR fix notes

PR #84180: fix: coerce HEAD to GET for internal images

Description (problem / solution / changelog)

Fixes: https://github.com/vercel/next.js/issues/84129

TODO:

  • e2e test (?)
  • verify against repro steps in source issue

Changed files

  • packages/next/src/server/image-optimizer.ts (modified, +10/-2)
  • test/e2e/app-dir/next-image/next-image.test.ts (modified, +32/-0)

Code Example

rm -rf .next/cache/images

---

curl -IL "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

---

HTTP/1.1 400 Bad Request
Date: Tue, 23 Sep 2025 09:44:29 GMT
Connection: keep-alive
Keep-Alive: timeout=5

---

The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null
The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null
The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null

---

curl -D -- -o test.webp "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

---

HTTP/1.1 200 OK
Vary: Accept
Cache-Control: public, max-age=315360000, immutable
ETag: RyS1td48qGQV0eM3i0m8noPJ3D5-WZfkAMCZjHfkHHo
Content-Type: image/jpeg
Content-Disposition: attachment; filename="test-image.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache: HIT
Content-Length: 134217
Date: Tue, 23 Sep 2025 09:47:14 GMT
Connection: keep-alive
Keep-Alive: timeout=5

---

curl -IL "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

---

HTTP/1.1 200 OK
Vary: Accept
Cache-Control: public, max-age=315360000, immutable
ETag: RyS1td48qGQV0eM3i0m8noPJ3D5-WZfkAMCZjHfkHHo
Content-Type: image/jpeg
Content-Disposition: attachment; filename="test-image.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache: HIT
Content-Length: 134217
Date: Tue, 23 Sep 2025 09:48:48 GMT
Connection: keep-alive
Keep-Alive: timeout=5

---

# Next 15.3.0

# clear next cache

# DO HTTP GET
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:42:28 GMT
Content-Length: 2043793
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS

# DO HTTP GET AGAIN
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:42:33 GMT
Content-Length: 2043793
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

# Stable, normal, cached the response with the correct content-length.

# I cleared the cache inside the container, now I make a HEAD request.

$ curl  -IL "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"
HTTP/1.1 200 OK
Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:43:23 GMT
Content-Length: 0 # <--- PROBLEM
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS # <--- CACHED AFTER THIS REQUEST

# DO HTTP GET
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:44:22 GMT
Content-Length: 0 # <--- PROBLEM
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

# Check downloaded file size
xdd test.jpg | head
# empty...
# Something went wrong. The first HEAD cached an empty response, and now GET returns it.

# I cleared the cache inside the container. 
# First, I do a GET to cache a normal response.
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:45:28 GMT
Content-Length: 2043793 # Normal response
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS

$ curl  -IL "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"
HTTP/1.1 200 OK
Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:45:31 GMT
Content-Length: 2043793 # # Normal response
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041
  Available memory (MB): 49152
  Available CPU cores: 12
Binaries:
  Node: 22.17.0
  npm: 10.9.2
  Yarn: N/A
  pnpm: 9.11.0
Relevant Packages:
  next: 15.5.3 // Latest available version is detected (15.5.3).
  eslint-config-next: 15.5.3
  react: 19.1.0
  react-dom: 19.1.0
  typescript: 5.9.2
Next.js Config:
  output: N/A
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/Git-I985/next-head-image-empty-cache

Description

When making a HEAD request to an image served via the Next.js Image Optimizer (/_next/image) before the image is cached, the server returns 400 Bad Request.

  • On Next.js 15.3.x:
    • A first HEAD request poisoned the cache by storing an empty file, causing subsequent GET requests to return an empty response.
  • On Next.js 15.5.x:
    • The cache poisoning seems fixed (no empty files are written).
    • However, the first uncached HEAD request still fails with 400 Bad Request.
    • Only after a GET request succeeds and the image is cached, subsequent HEAD requests return 200 OK.

This behavior breaks CDNs/proxies that issue HEAD requests for validation or preflight, since they receive a 400 instead of headers describing the resource.

To Reproduce

Current version

  1. Build and start npm run build && npm run start
  2. Clear cache
rm -rf .next/cache/images
  1. Make HTTP HEAD request to the image resource
curl -IL "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

Result (problem):

HTTP/1.1 400 Bad Request
Date: Tue, 23 Sep 2025 09:44:29 GMT
Connection: keep-alive
Keep-Alive: timeout=5
 ⨯ The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null
 ⨯ The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null
 ⨯ The requested resource isn't a valid image for /_next/static/media/test-image.daca9759.jpg received null
  1. Make HTTP GET request to the image resource
curl -D -- -o test.webp "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

Result

HTTP/1.1 200 OK
Vary: Accept
Cache-Control: public, max-age=315360000, immutable
ETag: RyS1td48qGQV0eM3i0m8noPJ3D5-WZfkAMCZjHfkHHo
Content-Type: image/jpeg
Content-Disposition: attachment; filename="test-image.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache: HIT
Content-Length: 134217
Date: Tue, 23 Sep 2025 09:47:14 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Now image cached properly and next HTTP HEAD request will be OK 5. Make HTTP HEAD request to the image resource again

curl -IL "http://localhost:3000/_next/image?url=/_next/static/media/test-image.daca9759.jpg&w=3840&q=75"

Result:

HTTP/1.1 200 OK
Vary: Accept
Cache-Control: public, max-age=315360000, immutable
ETag: RyS1td48qGQV0eM3i0m8noPJ3D5-WZfkAMCZjHfkHHo
Content-Type: image/jpeg
Content-Disposition: attachment; filename="test-image.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache: HIT
Content-Length: 134217
Date: Tue, 23 Sep 2025 09:48:48 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Older versions (reproducible on 15.3.X)

first HEAD request (when the image was not yet in the cache) poisoned the cache, and an empty file was stored there, so the following GET requests returned an empty file.

# Next 15.3.0

# clear next cache

# DO HTTP GET
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:42:28 GMT
Content-Length: 2043793
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS

# DO HTTP GET AGAIN
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:42:33 GMT
Content-Length: 2043793
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

# Stable, normal, cached the response with the correct content-length.

# I cleared the cache inside the container, now I make a HEAD request.

$ curl  -IL "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"
HTTP/1.1 200 OK
Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:43:23 GMT
Content-Length: 0 # <--- PROBLEM
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS # <--- CACHED AFTER THIS REQUEST

# DO HTTP GET
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:44:22 GMT
Content-Length: 0 # <--- PROBLEM
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

# Check downloaded file size
xdd test.jpg | head
# empty...
# Something went wrong. The first HEAD cached an empty response, and now GET returns it.

# I cleared the cache inside the container. 
# First, I do a GET to cache a normal response.
$ curl  -D - -o test.jpg "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"

Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:45:28 GMT
Content-Length: 2043793 # Normal response
Cache-Tag: nextjs-source
X-Nextjs-Cache: MISS

$ curl  -IL "http://localhost:4000/_next/image?url=/_next/static/media/file.e2ff9d29.jpg&w=3840&q=74&t=12"
HTTP/1.1 200 OK
Cache-Control: public, max-age=315360000, immutable
Vary: Accept
ETag: Vy8iMWYyZjkxLTE5OTcxOTNjZjIzIg
Content-Type: image/jpeg
Content-Disposition: attachment; filename="file.jpeg"
Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox;
Date: Mon, 22 Sep 2025 13:45:31 GMT
Content-Length: 2043793 # # Normal response
Cache-Tag: nextjs-source
X-Nextjs-Cache: HIT

Current vs. Expected behavior

Current (15.5.x):

  • First uncached HEAD request → 400 Bad Request with error logs (received null).
  • No image written to cache.
  • Only after a successful GET request, HEAD starts returning 200 OK.

Expected:

  • HEAD request should behave like a GET request but without streaming the body.
  • Even if the image is not cached yet, the first HEAD should:
  • Return 200 OK.
  • Include correct headers (Content-Type, Content-Length, ETag, etc.).
  • Either skip writing to cache or safely populate cache metadata without breaking.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041
  Available memory (MB): 49152
  Available CPU cores: 12
Binaries:
  Node: 22.17.0
  npm: 10.9.2
  Yarn: N/A
  pnpm: 9.11.0
Relevant Packages:
  next: 15.5.3 // Latest available version is detected (15.5.3).
  eslint-config-next: 15.5.3
  react: 19.1.0
  react-dom: 19.1.0
  typescript: 5.9.2
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Image (next/image), Not sure

Which stage(s) are affected? (Select all that apply)

Other (Deployed), next dev (local), next start (local)

Versions

  • Next.js 15.3.x → cache poisoned by HEAD (empty files stored).
  • Next.js 15.5.x → cache poisoning fixed, but HEAD requests still fail with 400 Bad Request.

Impact

  • CDNs/load balancers (e.g. Cloudflare, Fastly, etc.) that send HEAD may treat the image route as broken.
  • It prevents reliable header-only introspection of images before cache warming.

<sub>NEXT-4731</sub>

extent analysis

TL;DR

The most likely fix for the issue of HEAD requests returning 400 Bad Request for uncached images in Next.js 15.5.x is to apply a workaround that handles HEAD requests differently, possibly by implementing a custom middleware or modifying the existing image handling logic.

Guidance

  • Investigate the Next.js image handling logic to understand why HEAD requests are failing with 400 Bad Request for uncached images.
  • Consider implementing a custom middleware to handle HEAD requests differently, potentially by returning a 200 OK response with the correct headers without streaming the body.
  • Review the differences in behavior between Next.js 15.3.x and 15.5.x to understand how the cache poisoning issue was fixed and how it relates to the current issue.
  • Look into potential workarounds for CDNs/load balancers that send HEAD requests, such as configuring them to send GET requests instead or implementing a custom solution to handle HEAD requests.

Example

No specific code example is provided as the issue requires a deeper understanding of the Next.js image handling logic and potential custom middleware implementation.

Notes

The issue seems to be specific to Next.js 15.5.x, and the fix may require a custom solution or a modification to the existing image handling logic. The workaround may need to be tailored to the specific use case and requirements.

Recommendation

Apply a workaround that handles HEAD requests differently, potentially by implementing a custom middleware to return a 200 OK response with the correct headers without streaming the body. This approach allows for a more reliable header-only introspection of images before cache warming, which is essential for CDNs/load balancers that send HEAD requests.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix HEAD requests for Next.js image return 400 Bad Request when image is not yet cached [1 pull requests, 11 comments, 7 participants]