fix: coerce HEAD to GET for internal images (#84180)

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

TODO:

- [x] e2e test (?)
- [x] verify against repro steps in source issue

---------

Co-authored-by: Steven <[email protected]>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
This commit is contained in:
Joseph
2026-01-29 15:43:18 +00:00
committed by GitHub
co-authored by Steven vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
parent f1a047fd80
commit ab4b3726ea
2 changed files with 42 additions and 2 deletions
+10 -2
View File
@@ -900,9 +900,12 @@ export async function fetchInternalImage(
) => Promise<void>
): Promise<ImageUpstream> {
try {
// Coerce HEAD to GET to avoid issues with the image optimizer
const method = !_req.method || _req.method === 'HEAD' ? 'GET' : _req.method
const mocked = createRequestResponseMocks({
url: href,
method: _req.method || 'GET',
method,
socket: _req.socket,
})
@@ -1193,7 +1196,12 @@ export function sendResponse(
)
if (!result.finished) {
res.setHeader('Content-Length', Buffer.byteLength(buffer))
res.end(buffer)
// A response body must not be sent for HEAD requests
if (req.method === 'HEAD') {
res.end()
} else {
res.end(buffer)
}
}
}
@@ -1,4 +1,6 @@
import { nextTestSetup } from 'e2e-utils'
import fs from 'fs-extra'
import { join } from 'path'
describe('app dir - next-image', () => {
const { next, skipped } = nextTestSetup({
@@ -11,6 +13,36 @@ describe('app dir - next-image', () => {
}
describe('ssr content', () => {
it('should handle HEAD requests for uncached images', async () => {
const imagesDir = join(next.testDir, '.next/cache/images')
await fs.remove(imagesDir).catch(() => {})
const $ = await next.render$('/')
const imageUrl = $('#app-layout').attr('src')
const headRes = await next.fetch(imageUrl, { method: 'HEAD' })
expect(headRes.status).toBe(200)
expect(headRes.headers.get('content-type')).toMatch(/^image\//)
expect(headRes.headers.get('X-Nextjs-Cache')).toBe('MISS')
const contentLength = headRes.headers.get('content-length')
expect(Number(contentLength || '0')).toBeGreaterThan(0)
const headBody = await headRes.arrayBuffer()
expect(headBody.byteLength).toBe(0)
const getRes = await next.fetch(imageUrl)
expect(getRes.status).toBe(200)
expect(getRes.headers.get('content-type')).toMatch(/^image\//)
expect(getRes.headers.get('X-Nextjs-Cache')).toBe('HIT')
const getContentLength = getRes.headers.get('content-length')
expect(Number(getContentLength || '0')).toBeGreaterThan(0)
const getBody = await getRes.arrayBuffer()
expect(getBody.byteLength).toBeGreaterThan(0)
expect(getBody.byteLength).toBe(Number(getContentLength))
})
it('should render images on / route', async () => {
const $ = await next.render$('/')