Skip to content

Commit 4921ba1

Browse files
authored
Merge pull request #39 from cryptomator/feature/fix-webdav-task-registration-race
Fix WebDAV task registration race causing stuck downloads and uploads
2 parents 2730079 + 90ad667 commit 4921ba1

3 files changed

Lines changed: 375 additions & 4 deletions

File tree

Sources/CryptomatorCloudAccess/WebDAV/WebDAVSession.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,14 @@ class WebDAVSession {
221221
HTTPDebugLogger.logRequest(request)
222222
let progress = Progress(totalUnitCount: 1)
223223
let task = urlSession.downloadTask(with: request)
224-
onTaskCreation?(task)
225224
progress.addChild(task.progress, withPendingUnitCount: 1)
226225
let pendingPromise = Promise<HTTPURLResponse>.pending()
227226
let webDAVDownloadTask = WebDAVDownloadTask(promise: pendingPromise, localURL: localURL)
227+
// Register before invoking `onTaskCreation` so a synchronous resume cannot race the delegate's lookup.
228228
delegate?.addRunningDownloadTask(key: task, value: webDAVDownloadTask)
229-
if onTaskCreation == nil {
229+
if let onTaskCreation {
230+
onTaskCreation(task)
231+
} else {
230232
task.resume()
231233
}
232234
return pendingPromise
@@ -236,12 +238,14 @@ class WebDAVSession {
236238
HTTPDebugLogger.logRequest(request)
237239
let progress = Progress(totalUnitCount: 1)
238240
let task = urlSession.uploadTask(with: request, fromFile: fileURL)
239-
onTaskCreation?(task)
240241
progress.addChild(task.progress, withPendingUnitCount: 1)
241242
let pendingPromise = Promise<(HTTPURLResponse, Data?)>.pending()
242243
let webDAVDataTask = WebDAVDataTask(promise: pendingPromise)
244+
// Register before invoking `onTaskCreation` so a synchronous resume cannot race the delegate's lookup.
243245
delegate?.addRunningDataTask(key: task, value: webDAVDataTask)
244-
if onTaskCreation == nil {
246+
if let onTaskCreation {
247+
onTaskCreation(task)
248+
} else {
245249
task.resume()
246250
}
247251
return pendingPromise

Tests/CryptomatorCloudAccessTests/WebDAV/URLProtocolMock.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010

1111
enum URLProtocolMockError: Error {
1212
case unexpectedRequest
13+
case simulatedTransportFailure
1314
}
1415

1516
class URLProtocolMock: URLProtocol {
@@ -71,3 +72,47 @@ class URLProtocolAuthenticationMock: URLProtocol, URLAuthenticationChallengeSend
7172

7273
override func stopLoading() {}
7374
}
75+
76+
/// Test-only `URLProtocol` that emits a queued sequence of either HTTP responses or auth challenges
77+
/// in order. Used to target an auth challenge at a specific request (e.g. the transfer task) while
78+
/// letting earlier requests (e.g. the preflight `PROPFIND`) resolve normally.
79+
class URLProtocolSequenceMock: URLProtocol, URLAuthenticationChallengeSender {
80+
enum Step {
81+
case response(HTTPURLResponse, Data?)
82+
case authChallenge(URLAuthenticationChallengeMock)
83+
}
84+
85+
func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {}
86+
87+
func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {}
88+
89+
func cancel(_ challenge: URLAuthenticationChallenge) {}
90+
91+
static var steps = [Step]()
92+
93+
override func startLoading() {
94+
let step = URLProtocolSequenceMock.steps.removeFirst()
95+
switch step {
96+
case let .response(response, data):
97+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
98+
if let data = data {
99+
client?.urlProtocol(self, didLoad: data)
100+
}
101+
client?.urlProtocolDidFinishLoading(self)
102+
case let .authChallenge(settings):
103+
let protectionSpace = URLProtectionSpace(host: "", port: 443, protocol: nil, realm: nil, authenticationMethod: nil)
104+
let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: settings.previousFailureCount, failureResponse: settings.failureResponse, error: nil, sender: self)
105+
client?.urlProtocol(self, didReceive: challenge)
106+
}
107+
}
108+
109+
override class func canInit(with request: URLRequest) -> Bool {
110+
return true
111+
}
112+
113+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
114+
return request
115+
}
116+
117+
override func stopLoading() {}
118+
}

0 commit comments

Comments
 (0)