@@ -3,7 +3,13 @@ import {
33 fetchInternalImage ,
44 ImageError ,
55} from 'next/dist/server/image-optimizer'
6+ import { serveStatic } from 'next/dist/server/serve-static'
7+ import { EventEmitter } from 'events'
8+ import { promises as fs } from 'fs'
69import type { IncomingMessage , ServerResponse } from 'http'
10+ import type { Socket } from 'net'
11+ import { tmpdir } from 'os'
12+ import { join } from 'path'
713
814describe ( 'fetchInternalImage' , ( ) => {
915 describe ( 'non-2xx responses' , ( ) => {
@@ -206,4 +212,57 @@ describe('fetchInternalImage', () => {
206212 expect ( result . buffer . length ) . toBe ( maximumResponseBody )
207213 } )
208214 } )
215+ describe ( 'when the requesting client disconnects' , ( ) => {
216+ it ( 'should complete the internal fetch even if the requester socket is no longer writable' , async ( ) => {
217+ const size = 1024 * 1024
218+ const fileName = 'disconnect.png'
219+ const dir = await fs . mkdtemp ( join ( tmpdir ( ) , 'fetch-internal-image-' ) )
220+ await fs . writeFile ( join ( dir , fileName ) , Buffer . alloc ( size , 1 ) )
221+
222+ try {
223+ // Emulate a requester whose connection already went away. `send`
224+ // (used by `serveStatic`) consults `on-finished`, which treats a
225+ // response as finished as soon as `res.socket.writable` is false.
226+ const closedSocket = Object . assign ( new EventEmitter ( ) , {
227+ writable : false ,
228+ destroyed : true ,
229+ } ) as unknown as Socket
230+ const mockReq = {
231+ method : 'GET' ,
232+ socket : closedSocket ,
233+ } as unknown as IncomingMessage
234+ const mockRes = { } as ServerResponse
235+
236+ const handleRequest = ( req : IncomingMessage , res : ServerResponse ) =>
237+ serveStatic ( req , res , fileName , { root : dir } )
238+
239+ const result = await Promise . race ( [
240+ fetchInternalImage (
241+ `/${ fileName } ` ,
242+ mockReq ,
243+ mockRes ,
244+ 300_000_000 ,
245+ handleRequest
246+ ) ,
247+ new Promise < never > ( ( _resolve , reject ) => {
248+ const timer = setTimeout (
249+ ( ) =>
250+ reject (
251+ new Error (
252+ 'fetchInternalImage did not settle after the requester disconnected'
253+ )
254+ ) ,
255+ 5_000
256+ )
257+ timer . unref ( )
258+ } ) ,
259+ ] )
260+
261+ expect ( result . buffer . length ) . toBe ( size )
262+ expect ( result . contentType ) . toBe ( 'image/png' )
263+ } finally {
264+ await fs . rm ( dir , { recursive : true , force : true } )
265+ }
266+ } )
267+ } )
209268} )
0 commit comments