@@ -22,6 +22,7 @@ class StallTestHttpStack extends TestHttpStack {
2222 this . progressSequences = new Map ( )
2323 this . progressPromises = new Map ( )
2424 this . nextProgressSequence = null
25+ this . methodsToStall = new Set ( )
2526 }
2627
2728 /**
@@ -31,6 +32,14 @@ class StallTestHttpStack extends TestHttpStack {
3132 this . stallOnNextPatch = true
3233 }
3334
35+ /**
36+ * Configure the stack to simulate a stall for a specific HTTP method
37+ * @param {String } method - HTTP method to stall (e.g., 'POST', 'HEAD', 'PATCH')
38+ */
39+ simulateStallForMethod ( method ) {
40+ this . methodsToStall . add ( method )
41+ }
42+
3443 /**
3544 * Set a custom progress sequence for the next PATCH request
3645 * @param {Array } sequence - Array of {bytes: number, delay: number} objects
@@ -46,38 +55,62 @@ class StallTestHttpStack extends TestHttpStack {
4655 createRequest ( method , url ) {
4756 const req = super . createRequest ( method , url )
4857
49- if ( method === 'PATCH' ) {
58+ if ( this . methodsToStall . has ( method ) ) {
59+ this . _setupMethodStall ( req , method )
60+ this . methodsToStall . delete ( method )
61+ } else if ( method === 'PATCH' ) {
5062 this . _setupPatchRequest ( req )
5163 }
5264
5365 return req
5466 }
5567
68+ _setupMethodStall ( req , method ) {
69+ const originalAbort = req . abort . bind ( req )
70+
71+ req . send = async function ( body ) {
72+ this . body = body
73+ this . _requestPromise = new Promise ( ( resolve , reject ) => {
74+ this . _rejectRequest = reject
75+ this . _resolveRequest = resolve
76+ } )
77+
78+ if ( req . _onRequestSend ) {
79+ req . _onRequestSend ( this )
80+ }
81+
82+ return this . _requestPromise
83+ }
84+
85+ req . abort = function ( ) {
86+ if ( this . _rejectRequest ) {
87+ this . _rejectRequest ( new Error ( 'request aborted' ) )
88+ }
89+ originalAbort ( )
90+ }
91+ }
92+
5693 _setupPatchRequest ( req ) {
5794 const self = this
5895
59- // Handle complete stalls
6096 if ( this . stallOnNextPatch ) {
6197 this . stallOnNextPatch = false
6298 req . send = async function ( body ) {
6399 this . body = body
64100 if ( body ) {
65101 this . bodySize = await getBodySize ( body )
66- // Don't call progress handler to simulate a complete stall
67102 }
68103 this . _onRequestSend ( this )
69104 return this . _requestPromise
70105 }
71106 return
72107 }
73108
74- // Handle progress sequences
75109 if ( this . nextProgressSequence ) {
76110 this . progressSequences . set ( req , this . nextProgressSequence )
77111 this . nextProgressSequence = null
78112 }
79113
80- // Override respondWith to wait for progress events
81114 const originalRespondWith = req . respondWith . bind ( req )
82115 req . respondWith = async ( resData ) => {
83116 const progressPromise = self . progressPromises . get ( req )
@@ -88,7 +121,6 @@ class StallTestHttpStack extends TestHttpStack {
88121 originalRespondWith ( resData )
89122 }
90123
91- // Override send to handle progress sequences
92124 req . send = async function ( body ) {
93125 this . body = body
94126 if ( body ) {
@@ -115,7 +147,7 @@ class StallTestHttpStack extends TestHttpStack {
115147 progressHandler ( event . bytes )
116148 }
117149 resolve ( )
118- } , 10 ) // Small delay to ensure stall detector is started
150+ } , 10 )
119151 } )
120152 this . progressPromises . set ( req , progressPromise )
121153 }
@@ -126,7 +158,7 @@ class StallTestHttpStack extends TestHttpStack {
126158 progressHandler ( 0 )
127159 progressHandler ( bodySize )
128160 resolve ( )
129- } , 10 ) // Small delay to ensure stall detector is started
161+ } , 10 )
130162 } )
131163 this . progressPromises . set ( req , progressPromise )
132164 }
@@ -166,6 +198,47 @@ async function handleUploadCreation(testStack, location = '/uploads/12345') {
166198 return req
167199}
168200
201+ /**
202+ * Helper function to test stall detection for a specific request method
203+ */
204+ async function testStallDetectionForMethod ( method , uploadOptions = { } ) {
205+ const { enableDebugLog } = await import ( 'tus-js-client' )
206+ enableDebugLog ( )
207+
208+ const testStack = new StallTestHttpStack ( )
209+ testStack . simulateStallForMethod ( method )
210+
211+ const options = {
212+ httpStack : testStack ,
213+ stallDetection : {
214+ enabled : true ,
215+ checkInterval : 50 ,
216+ stallTimeout : 200 ,
217+ } ,
218+ retryDelays : null ,
219+ ...uploadOptions ,
220+ }
221+
222+ const { upload, options : testOptions } = createTestUpload ( options )
223+
224+ const originalLog = console . log
225+ let loggedMessage = ''
226+ console . log = ( message ) => {
227+ loggedMessage += message + '\n'
228+ }
229+
230+ upload . start ( )
231+
232+ const request = await testStack . nextRequest ( )
233+ expect ( request . method ) . toBe ( method )
234+
235+ const error = await testOptions . onError . toBeCalled ( )
236+
237+ console . log = originalLog
238+
239+ return { error, loggedMessage, request }
240+ }
241+
169242describe ( 'tus-stall-detection' , ( ) => {
170243 describe ( 'integration tests' , ( ) => {
171244 it ( "should not enable stall detection if HTTP stack doesn't support progress events" , async ( ) => {
@@ -393,5 +466,27 @@ describe('tus-stall-detection', () => {
393466 expect ( error . message ) . toContain ( 'stalled: no progress' )
394467 expect ( options . onProgress . calls . count ( ) ) . toBeGreaterThan ( 0 )
395468 } )
469+
470+ it ( 'should detect stalls during POST request (upload creation)' , async ( ) => {
471+ const { error, loggedMessage, request } = await testStallDetectionForMethod ( 'POST' )
472+
473+ expect ( request . url ) . toBe ( 'https://tus.io/uploads' )
474+ expect ( error . message ) . toContain ( 'request aborted' )
475+ expect ( error . message ) . toContain ( 'POST' )
476+ expect ( loggedMessage ) . toContain ( 'starting stall detection' )
477+ expect ( loggedMessage ) . toContain ( 'upload stalled' )
478+ } )
479+
480+ it ( 'should detect stalls during HEAD request (resuming upload)' , async ( ) => {
481+ const { error, loggedMessage, request } = await testStallDetectionForMethod ( 'HEAD' , {
482+ uploadUrl : 'https://tus.io/uploads/existing' ,
483+ } )
484+
485+ expect ( request . url ) . toBe ( 'https://tus.io/uploads/existing' )
486+ expect ( error . message ) . toContain ( 'request aborted' )
487+ expect ( error . message ) . toContain ( 'HEAD' )
488+ expect ( loggedMessage ) . toContain ( 'starting stall detection' )
489+ expect ( loggedMessage ) . toContain ( 'upload stalled' )
490+ } )
396491 } )
397492} )
0 commit comments