Skip to content

Commit f4c75b8

Browse files
committed
fix(ios): add proper error handling for upload operations
- Store upload continuations to propagate errors to callers - Implement didCompleteWithError delegate method to handle errors - Check HTTP response status codes and throw errors for non-2xx responses - Handle cancellation errors properly - Remove continuation immediately to prevent double-resume - Fix race condition by ensuring state initialization before task resume - Upload now waits for completion and properly propagates errors
1 parent d7ee586 commit f4c75b8

1 file changed

Lines changed: 76 additions & 22 deletions

File tree

ios/NitroFSFileUploader.swift

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
1313
private var uploadSessions: [String: URLSession] = [:]
1414
private var progressCallbacks: [String: ((Double, Double) -> Void)] = [:]
1515
private var taskToJobId: [Int: String] = [:]
16+
private var uploadContinuations: [String: CheckedContinuation<Void, Error>] = [:]
1617
private let taskQueue = DispatchQueue(label: "com.nitrofs.uploader")
1718

1819
init(fileManager: FileManager) {
@@ -48,34 +49,28 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
4849

4950
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
5051

51-
let task = session.uploadTask(with: request, fromFile: multipartFile) { _, response, error in
52-
defer {
53-
try? FileManager.default.removeItem(at: multipartFile)
54-
session.finishTasksAndInvalidate()
55-
self.taskQueue.async {
56-
self.uploadTasks.removeValue(forKey: jobId)
57-
self.uploadSessions.removeValue(forKey: jobId)
58-
self.progressCallbacks.removeValue(forKey: jobId)
59-
self.taskToJobId.removeValue(forKey: task.taskIdentifier)
60-
}
61-
}
62-
63-
// Note: Errors are silently ignored here since the jobId is returned immediately
64-
// The user can check progress via onProgress callback or cancel if needed
65-
}
52+
let task = session.uploadTask(with: request, fromFile: multipartFile)
6653

6754
// Use sync to ensure state is initialized before task.resume() is called
6855
// This prevents race condition where delegate callbacks fire before state is set up
69-
taskQueue.sync {
70-
self.uploadTasks[jobId] = task
71-
self.uploadSessions[jobId] = session
72-
self.taskToJobId[task.taskIdentifier] = jobId
73-
if let onProgress = onProgress {
74-
self.progressCallbacks[jobId] = onProgress
56+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
57+
taskQueue.sync {
58+
self.uploadTasks[jobId] = task
59+
self.uploadSessions[jobId] = session
60+
self.taskToJobId[task.taskIdentifier] = jobId
61+
self.uploadContinuations[jobId] = continuation
62+
if let onProgress = onProgress {
63+
self.progressCallbacks[jobId] = onProgress
64+
}
7565
}
66+
task.resume()
67+
}
68+
69+
// Cleanup multipart file after completion
70+
defer {
71+
try? FileManager.default.removeItem(at: multipartFile)
7672
}
7773

78-
task.resume()
7974
return jobId
8075
}
8176

@@ -85,6 +80,11 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
8580
guard let self = self else { return }
8681
if let task = self.uploadTasks[jobId] {
8782
task.cancel()
83+
// Resume continuation with cancellation error if it exists
84+
if let continuation = self.uploadContinuations[jobId] {
85+
continuation.resume(throwing: NitroFSError.networkError(message: "Upload cancelled"))
86+
self.uploadContinuations.removeValue(forKey: jobId)
87+
}
8888
self.taskToJobId.removeValue(forKey: task.taskIdentifier)
8989
self.uploadTasks.removeValue(forKey: jobId)
9090
self.progressCallbacks.removeValue(forKey: jobId)
@@ -177,6 +177,60 @@ extension NitroFSFileUploader: URLSessionTaskDelegate {
177177
}
178178
}
179179
}
180+
181+
func urlSession(
182+
_ session: URLSession,
183+
task: URLSessionTask,
184+
didCompleteWithError error: Error?
185+
) {
186+
var jobId: String?
187+
var continuation: CheckedContinuation<Void, Error>?
188+
189+
taskQueue.sync {
190+
jobId = self.taskToJobId[task.taskIdentifier]
191+
if let jobId = jobId {
192+
continuation = self.uploadContinuations[jobId]
193+
}
194+
}
195+
196+
guard let jobId = jobId else { return }
197+
198+
// Remove continuation immediately to prevent double-resume
199+
taskQueue.sync {
200+
self.uploadContinuations.removeValue(forKey: jobId)
201+
}
202+
203+
if let error = error {
204+
// Handle cancellation separately
205+
if (error as NSError).code == NSURLErrorCancelled {
206+
continuation?.resume(throwing: NitroFSError.networkError(message: "Upload cancelled"))
207+
} else {
208+
continuation?.resume(throwing: error)
209+
}
210+
} else {
211+
// Check HTTP response status
212+
if let httpResponse = task.response as? HTTPURLResponse {
213+
guard (200...299).contains(httpResponse.statusCode) else {
214+
continuation?.resume(throwing: NitroFSError.networkError(
215+
message: "Upload failed with status code: \(httpResponse.statusCode)"
216+
))
217+
return
218+
}
219+
}
220+
// Success - resume continuation
221+
continuation?.resume()
222+
}
223+
224+
// Cleanup
225+
taskQueue.async {
226+
self.uploadTasks.removeValue(forKey: jobId)
227+
self.uploadSessions.removeValue(forKey: jobId)
228+
self.progressCallbacks.removeValue(forKey: jobId)
229+
self.taskToJobId.removeValue(forKey: task.taskIdentifier)
230+
}
231+
232+
session.finishTasksAndInvalidate()
233+
}
180234
}
181235

182236
// MARK: - OutputStream Extension

0 commit comments

Comments
 (0)