Skip to content

Commit 44b87ab

Browse files
committed
Ensure stallDetector is supplied for HEAD and POST requests
1 parent e4175c3 commit 44b87ab

File tree

2 files changed

+131
-23
lines changed

2 files changed

+131
-23
lines changed

lib/upload.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,9 @@ export class BaseUpload {
407407

408408
let res: HttpResponse
409409
try {
410-
res = await this._sendRequest(req)
410+
// Create stall detector for final concatenation POST request
411+
const stallDetector = this._createStallDetector()
412+
res = await this._sendRequest(req, undefined, stallDetector)
411413
} catch (err) {
412414
if (!(err instanceof Error)) {
413415
throw new Error(`tus: value thrown that is not an error: ${err}`)
@@ -638,7 +640,9 @@ export class BaseUpload {
638640
) {
639641
req.setHeader('Upload-Complete', '?0')
640642
}
641-
res = await this._sendRequest(req)
643+
// Create stall detector for POST request
644+
const stallDetector = this._createStallDetector()
645+
res = await this._sendRequest(req, undefined, stallDetector)
642646
}
643647
} catch (err) {
644648
if (!(err instanceof Error)) {
@@ -700,7 +704,9 @@ export class BaseUpload {
700704

701705
let res: HttpResponse
702706
try {
703-
res = await this._sendRequest(req)
707+
// Create stall detector for HEAD request
708+
const stallDetector = this._createStallDetector()
709+
res = await this._sendRequest(req, undefined, stallDetector)
704710
} catch (err) {
705711
if (!(err instanceof Error)) {
706712
throw new Error(`tus: value thrown that is not an error: ${err}`)
@@ -852,23 +858,15 @@ export class BaseUpload {
852858
}
853859

854860
/**
855-
* _addChunktoRequest reads a chunk from the source and sends it using the
856-
* supplied request object. It will not handle the response.
861+
* Create a stall detector if stall detection is enabled and supported.
857862
*
858863
* @api private
859864
*/
860-
private async _addChunkToRequest(req: HttpRequest): Promise<HttpResponse> {
861-
const start = this._offset
862-
let end = this._offset + this.options.chunkSize
863-
864-
// Create stall detector for this request if stall detection is enabled and supported
865-
// but don't start it yet - we'll start it after onBeforeRequest completes
866-
let stallDetector: StallDetector | undefined
867-
865+
private _createStallDetector(): StallDetector | undefined {
868866
if (this.options.stallDetection?.enabled) {
869867
// Only enable stall detection if the HTTP stack supports progress events
870868
if (this.options.httpStack.supportsProgressEvents()) {
871-
stallDetector = new StallDetector(this.options.stallDetection, (reason: string) => {
869+
return new StallDetector(this.options.stallDetection, (reason: string) => {
872870
// Handle stall by aborting the current request
873871
// The abort will cause the request to fail, which will be caught
874872
// in _performUpload and wrapped in a DetailedError for proper retry handling
@@ -878,13 +876,28 @@ export class BaseUpload {
878876
}
879877
// Don't call _retryOrEmitError here - let the natural error flow handle it
880878
})
881-
// Don't start yet - will be started after onBeforeRequest
882879
} else {
883880
log(
884881
'tus: stall detection is enabled but the HTTP stack does not support progress events, it will be disabled for this upload',
885882
)
886883
}
887884
}
885+
return undefined
886+
}
887+
888+
/**
889+
* _addChunktoRequest reads a chunk from the source and sends it using the
890+
* supplied request object. It will not handle the response.
891+
*
892+
* @api private
893+
*/
894+
private async _addChunkToRequest(req: HttpRequest): Promise<HttpResponse> {
895+
const start = this._offset
896+
let end = this._offset + this.options.chunkSize
897+
898+
// Create stall detector for this request if stall detection is enabled and supported
899+
// but don't start it yet - we'll start it after onBeforeRequest completes
900+
const stallDetector = this._createStallDetector()
888901

889902
req.setProgressHandler((bytesSent) => {
890903
// Update per-request stall detector if active

test/spec/test-stall-detection.js

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
169242
describe('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

Comments
 (0)