fix: (regression 0.57.9) resume-upload broken on firefox and safari
Massimo Melina committed
Aug 7, 2025 at 14:57 UTC
ee76fb434bd3071acb32b0ce18aafb17d4cec3e4
2 files changed
+21
-5
frontend/src/uploadQueue.ts
+16
-3
@@ -101,14 +101,26 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
101
const uriPath = to + pathEncode(toUpload.path)
102
let stopLooping = false // allow callbacks to stop the loop
103
do { // at least one iteration, even for empty files
104
- const req = currentReq = new XMLHttpRequest()
104
+ let req = currentReq = new XMLHttpRequest()
105
const requestIsOver = pendingPromise()
106
stuckSince = Date.now()
107
// beware of 'abort' event: it isn't triggered if connection isn't established yet
108
- req.onloadend = async () => {
108
+ req.onloadend = async () => { // loadend = fired for both success and error. Safari doesn't always fire this on disconnections, leaving readyState = 3. The problem is mitigated by the abort-when-stuck mechanism above.
109
try {
110
currentReq = undefined
111
strictResume = true // reset at each request
112
+ if (!userAborted && !req.status) { // we were disconnected, possibly with a status that we couldn't read, so we give it another chance without the body
113
+ /* Browsers are unreliable when it comes to read the status before the request is fully sent.
114
+ - chrome139 works for most of the cases. It seems it doesn't when the disconnection happens a bit late (like for file system errors).
115
+ - safari18 is inconsistent and it seems random.
116
+ - firefox141 basically never works.
117
+ */
118
+ req = new XMLHttpRequest()
119
+ req.open('PUT', uriPath + queryString + '&simulating=' + body.size, false) // not async this time
120
+ try { req.send() }
121
+ catch(e) { console.log(e) }
122
+ await wait(500) // on a fast connection (localhost) firefox is aborting next request (without the delay), reporting NS_BINDING_ABORTED. Still don't know why
123
+ }
124
const { status } = req
125
if (status === HTTP_RANGE_NOT_SATISFIABLE)
126
return stopLooping = true
@@ -171,7 +183,8 @@ export async function startUpload(toUpload: ToUpload, to: string, resume=0) {
183
existing: with_(state.uploadOnExisting, x => x !== 'rename' ? x : undefined), // rename is the default
184
})
185
req.open('PUT', uriPath + queryString, true)
174
- req.send(toUpload.file.slice(resume, splitSize ? resume + splitSize : undefined))
186
+ const body = toUpload.file.slice(resume, splitSize ? resume + splitSize : undefined)
187
+ req.send(body)
188
await requestIsOver
189
} while (!stopLooping)
190
src/upload.ts
+5
-2
@@ -71,7 +71,8 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
71
const dir = dirname(fullPath)
72
// enforce minAvailableMb
73
const min = minAvailableMb.get() * (1 << 20)
74
- const contentLength = Number(ctx.headers["content-length"])
74
+ const {simulate} = ctx.query // `simulate` is used to get the same error but with an empty body, so that the request is processed quickly
75
+ const contentLength = Number(simulate || ctx.headers["content-length"])
76
const isPartial = ctx.query.partial !== undefined // while the presence of "partial" conveys the upload is split...
77
const stillToWrite = Math.max(contentLength, Number(ctx.query.partial) || 0) // ...the number is used to tell how much space we need (fullSize - offset)
78
if (isNaN(stillToWrite)) {
@@ -155,6 +156,8 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
156
try {
157
ctx.state.uploadSize = bytesGot() // in case content-length is not specified
158
await new Promise(res => fileStream.close(res)) // this only seems necessary on Windows
159
+ if (simulate)
160
+ return rm(tempName).catch(() => {})
161
if (ctx.isAborted()) { // in the very unlikely case the connection is interrupted between last-byte and here, we still consider it unfinished, as the client had no way to know, and will resume, but it would get an error if we finish the process
162
const sec = deleteUnfinishedUploadsAfter.get()
163
return _.isNumber(sec) && delayedDelete(tempName, sec)
@@ -243,7 +246,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
246
expires: Date.now() + secs * 1000,
247
timeout: setTimeout(() => {
248
delete waitingToBeDeleted[path]
246
- void rm(path)
249
+ rm(path).catch(() => {})
250
}, secs * 1000)
251
}
252
}