Skip to content

Commit 9cf7aea

Browse files
authored
fix tus upload rejection handling (#438)
1 parent a3b9b56 commit 9cf7aea

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@transloadit/node": patch
3+
"@transloadit/mcp-server": patch
4+
"transloadit": patch
5+
---
6+
7+
Handle tus upload failures without leaving duplicate internal promises unhandled.

‎packages/node/src/tus.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ export async function sendTusRequest({
153153
rejectCompletion = reject
154154
})
155155

156+
// If startPromise rejects first, pMap aborts before sendTusRequest reaches the aggregate awaits
157+
// below. These bookkeeping promises still need handlers so the startPromise rejection remains
158+
// the single error surface.
159+
uploadUrlPromise.catch(() => {})
160+
completionPromise.catch(() => {})
161+
156162
uploadUrlPromises.push(uploadUrlPromise)
157163
completionPromises.push(completionPromise)
158164

‎packages/node/test/unit/tus.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { stat as NodeStat } from 'node:fs/promises'
33
import type { OnSuccessPayload, UploadOptions } from 'tus-js-client'
44

55
import { PassThrough } from 'node:stream'
6+
import { setImmediate as delayImmediate } from 'node:timers/promises'
67

78
import { beforeEach, describe, expect, it, vi } from 'vitest'
89

@@ -270,4 +271,40 @@ describe('sendTusRequest', () => {
270271

271272
expect(uploadCtor).not.toHaveBeenCalled()
272273
})
274+
275+
it('does not leave bookkeeping promises unhandled when an awaited upload fails', async () => {
276+
statMock.mockResolvedValue({ size: 2048 })
277+
278+
const uploadError = new Error('connect ETIMEDOUT 5.161.182.178:443')
279+
const unhandledRejections: unknown[] = []
280+
const onUnhandledRejection = (reason: unknown): void => {
281+
unhandledRejections.push(reason)
282+
}
283+
284+
startBehavior = (options) => {
285+
options.onError?.(uploadError)
286+
}
287+
288+
process.on('unhandledRejection', onUnhandledRejection)
289+
try {
290+
await expect(
291+
sendTusRequest({
292+
streamsMap: {
293+
failed: { path: '/tmp/failed', stream: new PassThrough() },
294+
},
295+
assembly: baseAssembly,
296+
requestedChunkSize: 1_048_576,
297+
uploadConcurrency: 1,
298+
onProgress: () => {},
299+
}),
300+
).rejects.toBe(uploadError)
301+
302+
await delayImmediate()
303+
await delayImmediate()
304+
305+
expect(unhandledRejections).toEqual([])
306+
} finally {
307+
process.off('unhandledRejection', onUnhandledRejection)
308+
}
309+
})
273310
})

0 commit comments

Comments
 (0)