mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
dcbfff7789
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
/* eslint-env jest */
|
|
import {
|
|
fetchInternalImage,
|
|
ImageError,
|
|
} from 'next/dist/server/image-optimizer'
|
|
import { serveStatic } from 'next/dist/server/serve-static'
|
|
import { EventEmitter } from 'events'
|
|
import { promises as fs } from 'fs'
|
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
import type { Socket } from 'net'
|
|
import { tmpdir } from 'os'
|
|
import { join } from 'path'
|
|
|
|
describe('fetchInternalImage', () => {
|
|
describe('non-2xx responses', () => {
|
|
it('should throw error when the internal response is a redirect', async () => {
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
const maximumResponseBody = 300_000_000
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 307
|
|
res.getHeader = jest.fn(() => 'text/plain')
|
|
res.write('/redirected')
|
|
res.end()
|
|
})
|
|
|
|
const error = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
).catch((e) => e)
|
|
|
|
expect(error).toBeInstanceOf(ImageError)
|
|
// ImageError maps any status below 400 to 500
|
|
expect((error as ImageError).statusCode).toBe(500)
|
|
expect((error as ImageError).message).toBe(
|
|
'"url" parameter is valid but internal response is invalid'
|
|
)
|
|
})
|
|
|
|
it('should throw error when the internal response is a 404', async () => {
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
const maximumResponseBody = 300_000_000
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 404
|
|
res.getHeader = jest.fn(() => 'text/html')
|
|
res.write('<html>not found</html>')
|
|
res.end()
|
|
})
|
|
|
|
const error = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
).catch((e) => e)
|
|
|
|
expect(error).toBeInstanceOf(ImageError)
|
|
expect((error as ImageError).statusCode).toBe(404)
|
|
expect((error as ImageError).message).toBe(
|
|
'"url" parameter is valid but internal response is invalid'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('response size limit', () => {
|
|
it('should throw error when response has no buffers', async () => {
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
const maximumResponseBody = 300_000_000
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 200
|
|
res.getHeader = jest.fn(() => 'image/jpeg')
|
|
res.end()
|
|
})
|
|
|
|
const error = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
).catch((e) => e)
|
|
|
|
expect(error).toBeInstanceOf(ImageError)
|
|
expect((error as ImageError).statusCode).toBe(400)
|
|
expect((error as ImageError).message).toBe(
|
|
'"url" parameter is valid but internal response is invalid'
|
|
)
|
|
})
|
|
|
|
it('should throw error when exceeding maximumResponseBody config on later chunk', async () => {
|
|
const maximumResponseBody = 2_000 // 2KB custom limit
|
|
const chunkSize = 1_000 // 1KB chunks
|
|
const numChunks = 3 // 3KB total, exceeds custom 2KB limit
|
|
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 200
|
|
res.getHeader = jest.fn(() => 'image/jpeg')
|
|
|
|
for (let i = 0; i < numChunks; i++) {
|
|
res.write(Buffer.alloc(chunkSize))
|
|
}
|
|
res.end()
|
|
})
|
|
|
|
const error = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
).catch((e) => e)
|
|
|
|
expect(error).toBeInstanceOf(ImageError)
|
|
expect((error as ImageError).statusCode).toBe(413)
|
|
expect((error as ImageError).message).toBe(
|
|
'"url" parameter is valid but internal response is invalid'
|
|
)
|
|
})
|
|
|
|
it('should throw error when exceeding maximumResponseBody config on first chunk', async () => {
|
|
const maximumResponseBody = 2_000 // 2KB custom limit
|
|
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 200
|
|
res.getHeader = jest.fn(() => 'image/jpeg')
|
|
res.write(Buffer.alloc(maximumResponseBody + 1))
|
|
res.end()
|
|
})
|
|
|
|
const error = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
).catch((e) => e)
|
|
|
|
expect(error).toBeInstanceOf(ImageError)
|
|
expect((error as ImageError).statusCode).toBe(413)
|
|
expect((error as ImageError).message).toBe(
|
|
'"url" parameter is valid but internal response is invalid'
|
|
)
|
|
})
|
|
|
|
it('should succeed when exactly matching maximumResponseBody config on first chunk', async () => {
|
|
const maximumResponseBody = 3_000 // 3KB custom limit
|
|
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 200
|
|
res.getHeader = jest.fn(() => 'image/jpeg')
|
|
res.write(Buffer.alloc(maximumResponseBody))
|
|
res.end()
|
|
})
|
|
|
|
const result = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
)
|
|
|
|
expect(result.buffer).toBeInstanceOf(Buffer)
|
|
expect(result.buffer.length).toBe(maximumResponseBody)
|
|
})
|
|
|
|
it('should succeed when exactly matching maximumResponseBody config on later chunk', async () => {
|
|
const maximumResponseBody = 3_000 // 3KB custom limit
|
|
const chunkSize = 1_000 // 1KB chunks
|
|
const numChunks = 3 // 3KB total
|
|
|
|
const mockReq = {} as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
|
|
const handleRequest = jest.fn(async (_req: IncomingMessage, res: any) => {
|
|
res.statusCode = 200
|
|
res.getHeader = jest.fn(() => 'image/jpeg')
|
|
|
|
for (let i = 0; i < numChunks; i++) {
|
|
res.write(Buffer.alloc(chunkSize))
|
|
}
|
|
res.end()
|
|
})
|
|
|
|
const result = await fetchInternalImage(
|
|
'/test-image.jpg',
|
|
mockReq,
|
|
mockRes,
|
|
maximumResponseBody,
|
|
handleRequest
|
|
)
|
|
|
|
expect(result.buffer).toBeInstanceOf(Buffer)
|
|
expect(result.buffer.length).toBe(maximumResponseBody)
|
|
})
|
|
})
|
|
describe('when the requesting client disconnects', () => {
|
|
it('should complete the internal fetch even if the requester socket is no longer writable', async () => {
|
|
const size = 1024 * 1024
|
|
const fileName = 'disconnect.png'
|
|
const dir = await fs.mkdtemp(join(tmpdir(), 'fetch-internal-image-'))
|
|
await fs.writeFile(join(dir, fileName), Buffer.alloc(size, 1))
|
|
|
|
try {
|
|
// Emulate a requester whose connection already went away. `send`
|
|
// (used by `serveStatic`) consults `on-finished`, which treats a
|
|
// response as finished as soon as `res.socket.writable` is false.
|
|
const closedSocket = Object.assign(new EventEmitter(), {
|
|
writable: false,
|
|
destroyed: true,
|
|
}) as unknown as Socket
|
|
const mockReq = {
|
|
method: 'GET',
|
|
socket: closedSocket,
|
|
} as unknown as IncomingMessage
|
|
const mockRes = {} as ServerResponse
|
|
|
|
const handleRequest = (req: IncomingMessage, res: ServerResponse) =>
|
|
serveStatic(req, res, fileName, { root: dir })
|
|
|
|
const result = await Promise.race([
|
|
fetchInternalImage(
|
|
`/${fileName}`,
|
|
mockReq,
|
|
mockRes,
|
|
300_000_000,
|
|
handleRequest
|
|
),
|
|
new Promise<never>((_resolve, reject) => {
|
|
const timer = setTimeout(
|
|
() =>
|
|
reject(
|
|
new Error(
|
|
'fetchInternalImage did not settle after the requester disconnected'
|
|
)
|
|
),
|
|
5_000
|
|
)
|
|
timer.unref()
|
|
}),
|
|
])
|
|
|
|
expect(result.buffer.length).toBe(size)
|
|
expect(result.contentType).toBe('image/png')
|
|
} finally {
|
|
await fs.rm(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|
|
})
|