Skip to content

Commit eabecf3

Browse files
authored
Fix image content type octet stream 400 (#26705)
Fixes #23523 by adding image content type detection ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added
1 parent d11589d commit eabecf3

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

packages/next/next-server/server/image-optimizer.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ export async function imageOptimizer(
198198

199199
res.statusCode = upstreamRes.status
200200
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer())
201-
upstreamType = upstreamRes.headers.get('Content-Type')
201+
upstreamType =
202+
detectContentType(upstreamBuffer) ||
203+
upstreamRes.headers.get('Content-Type')
202204
maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
203205
} else {
204206
try {
@@ -252,7 +254,8 @@ export async function imageOptimizer(
252254
res.statusCode = mockRes.statusCode
253255

254256
upstreamBuffer = Buffer.concat(resBuffers)
255-
upstreamType = mockRes.getHeader('Content-Type')
257+
upstreamType =
258+
detectContentType(upstreamBuffer) || mockRes.getHeader('Content-Type')
256259
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'))
257260
} catch (err) {
258261
res.statusCode = 500
@@ -273,7 +276,6 @@ export async function imageOptimizer(
273276
return { finished: true }
274277
}
275278

276-
// If upstream type is not a valid image type, return 400 error.
277279
if (!upstreamType.startsWith('image/')) {
278280
res.statusCode = 400
279281
res.end("The requested resource isn't a valid image.")
@@ -426,6 +428,38 @@ function parseCacheControl(str: string | null): Map<string, string> {
426428
return map
427429
}
428430

431+
/**
432+
* Inspects the first few bytes of a buffer to determine if
433+
* it matches the "magic number" of known file signatures.
434+
* https://en.wikipedia.org/wiki/List_of_file_signatures
435+
*/
436+
function detectContentType(buffer: Buffer) {
437+
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
438+
return JPEG
439+
}
440+
if (
441+
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every(
442+
(b, i) => buffer[i] === b
443+
)
444+
) {
445+
return PNG
446+
}
447+
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
448+
return GIF
449+
}
450+
if (
451+
[0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every(
452+
(b, i) => !b || buffer[i] === b
453+
)
454+
) {
455+
return WEBP
456+
}
457+
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
458+
return SVG
459+
}
460+
return null
461+
}
462+
429463
export function getMaxAge(str: string | null): number {
430464
const minimum = 60
431465
const map = parseCacheControl(str)

test/integration/image-optimizer/test/index.test.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,26 @@ function runTests({ w, isDev, domains }) {
330330
expect(res.headers.get('etag')).toBeTruthy()
331331
await expectWidth(res, w)
332332
})
333+
334+
it('should automatically detect image type when content-type is octet-stream', async () => {
335+
const url =
336+
'https://image-optimization-test.vercel.app/png-as-octet-stream'
337+
const resOrig = await fetch(url)
338+
expect(resOrig.status).toBe(200)
339+
expect(resOrig.headers.get('Content-Type')).toBe(
340+
'application/octet-stream'
341+
)
342+
const query = { url, w, q: 80 }
343+
const opts = { headers: { accept: 'image/webp' } }
344+
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
345+
expect(res.status).toBe(200)
346+
expect(res.headers.get('Content-Type')).toBe('image/webp')
347+
expect(res.headers.get('cache-control')).toBe(
348+
'public, max-age=0, must-revalidate'
349+
)
350+
expect(res.headers.get('etag')).toBeTruthy()
351+
await expectWidth(res, w)
352+
})
333353
}
334354

335355
it('should fail when url has file protocol', async () => {
@@ -697,7 +717,11 @@ describe('Image Optimizer', () => {
697717
})
698718

699719
// domains for testing
700-
const domains = ['localhost', 'example.com']
720+
const domains = [
721+
'localhost',
722+
'example.com',
723+
'image-optimization-test.vercel.app',
724+
]
701725

702726
describe('dev support w/o next.config.js', () => {
703727
const size = 384 // defaults defined in server/config.ts

0 commit comments

Comments
 (0)