• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 28015504651

23 Jun 2026 09:13AM UTC coverage: 80.889%. First build
28015504651

push

github

web-flow
feat(storage): add TUS resumable uploads, background downloads, and multipart upload engine (#988)

* docs: add storage background transfers design spec

Covers TUS resumable uploads, background URLSession downloads,
StorageTransferTask<Success> public API, error handling, and test plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add storage background transfers implementation plan

9-task TDD plan covering StorageTransferTask types, TUSUploadEngine,
DownloadSessionDelegate, StorageFileAPI wiring, and integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(storage): add StorageTransferTask, TransferEvent, TransferProgress types

Introduces core transfer infrastructure: StorageTransferTask<Success> generic
handle with events stream and async result, TransferEvent and TransferProgress
value types, mapResult transformation, StorageErrorCode/StorageError extensions
for networkError, fileSystemError, and cancelled, and typed aliases
StorageUploadTask and StorageDownloadTask.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(storage): fix mapResult bridge Task leak, add missing tests

Store the bridge Task in mapResult and cancel it when the returned task
is cancelled to prevent the loop from leaking. Add terminal doc comment
to .completed, and add tests for cancel closure invocation and transform
error propagation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(storage): add backgroundDownloadSessionIdentifier config + download session setup

Adds `backgroundDownloadSessionIdentifier` to `StorageClientConfiguration`,
creates a stub `DownloadSessionDelegate`, and wires a `URLSession` download
session (foreground or background) into `StorageClient` with a
`handleBackgroundEvents(forSessionIdentifier:completionHandler:)` entry point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(storage): add TUSUploadEngine with POST create and PATCH chunk loop

Co... (continued)

982 of 1143 new or added lines in 11 files covered. (85.91%)

8279 of 10235 relevant lines covered (80.89%)

34.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.11
/Sources/Storage/StorageError.swift
1
//
2
//  StorageError.swift
3
//  Storage
4
//
5
//  Created by Guilherme Souza on 22/05/24.
6
//
7

8
import Foundation
9

10
#if canImport(FoundationNetworking)
11
  import FoundationNetworking
12
#endif
13

14
/// A typed code identifying the specific error returned by the Storage server.
15
///
16
/// Known server error strings are exposed as static constants. Because the server may return codes
17
/// not listed here (e.g. when the SDK is older than the server), the type is open-ended: any
18
/// unrecognised string is representable without breaking existing `switch` statements.
19
///
20
/// ## Example
21
///
22
/// ```swift
23
/// catch let error as StorageError {
24
///   if error.errorCode == .objectNotFound { /* handle missing object */ }
25
/// }
26
/// ```
27
// Intentionally not Decodable — server JSON decoding is handled by the private
28
// ServerErrorResponse struct in StorageClient, which constructs StorageErrorCode values directly.
29
public struct StorageErrorCode: RawRepresentable, Sendable, Hashable {
30
  public var rawValue: String
31

32
  public init(rawValue: String) {
13✔
33
    self.rawValue = rawValue
13✔
34
  }
13✔
35

36
  public init(_ rawValue: String) {
12✔
37
    self.init(rawValue: rawValue)
12✔
38
  }
12✔
39
}
40

41
extension StorageErrorCode {
42
  /// Fallback used when the server returns an unrecognised code or a non-JSON body.
43
  public static let unknown = StorageErrorCode("unknown")
44

45
  // MARK: - Authentication / authorisation
46

47
  /// The JWT supplied with the request is invalid.
48
  public static let invalidJWT = StorageErrorCode("InvalidJWT")
49
  /// The request was rejected because the caller is not authorised.
50
  public static let unauthorized = StorageErrorCode("Unauthorized")
51

52
  // MARK: - Object / bucket
53

54
  /// The requested object does not exist.
55
  public static let objectNotFound = StorageErrorCode("not_found")
56
  /// The requested bucket does not exist.
57
  public static let bucketNotFound = StorageErrorCode("Bucket not found")
58
  /// An object at the given path already exists and upsert was not requested.
59
  public static let objectAlreadyExists = StorageErrorCode("Duplicate")
60
  /// A bucket with the given name already exists.
61
  ///
62
  /// The server emits the same `"Duplicate"` wire value for both bucket and object conflicts;
63
  /// use the calling context to distinguish them.
64
  public static let bucketAlreadyExists = StorageErrorCode("Duplicate")
65
  /// The bucket name does not meet naming requirements.
66
  public static let invalidBucketName = StorageErrorCode("Invalid Input")
67

68
  // MARK: - Upload
69

70
  /// The uploaded file exceeds the configured size limit.
71
  public static let entityTooLarge = StorageErrorCode("Payload too large")
72
  /// The MIME type of the uploaded file is not allowed.
73
  public static let invalidMimeType = StorageErrorCode("invalid_mime_type")
74

75
  // MARK: - Client-side synthetic codes (no HTTP response)
76

77
  /// The signed upload URL returned by the server contained no upload token.
78
  public static let noTokenReturned = StorageErrorCode("noTokenReturned")
79
}
80

81
/// An error thrown by the Supabase Storage API.
82
///
83
/// All Storage operations throw ``StorageError`` on failure. Use ``message`` for a human-readable
84
/// description, ``errorCode`` to identify the specific failure kind, and ``statusCode`` for the
85
/// HTTP status when the error originated from a server response.
86
///
87
/// Adding new ``StorageErrorCode`` constants in future SDK versions is not a breaking change.
88
///
89
/// ## Example
90
///
91
/// ```swift
92
/// do {
93
///   try await storage.from("avatars").upload("image.png", data: data)
94
/// } catch let error as StorageError {
95
///   switch error.errorCode {
96
///   case .objectAlreadyExists:
97
///     print("File already exists — use upsert: true to overwrite")
98
///   case .entityTooLarge:
99
///     print("File is too large")
100
///   default:
101
///     print("Storage error \(error.statusCode ?? -1): \(error.message)")
102
///   }
103
/// }
104
/// ```
105
public struct StorageError: Error, Sendable {
106
  /// A human-readable description of what went wrong.
107
  public let message: String
108

109
  /// A typed error code identifying the specific failure.
110
  ///
111
  /// Set to ``StorageErrorCode/unknown`` when the server returns an unrecognised code or a
112
  /// non-JSON response body.
113
  public let errorCode: StorageErrorCode
114

115
  /// The HTTP status code returned by the server.
116
  ///
117
  /// `nil` for client-side errors that have no associated HTTP response
118
  /// (e.g. ``StorageError/noTokenReturned``).
119
  public let statusCode: Int?
120

121
  /// The raw HTTP response, available for advanced debugging.
122
  ///
123
  /// `nil` for client-side errors.
124
  public let underlyingResponse: HTTPURLResponse?
125

126
  /// The raw response body, available for advanced debugging.
127
  ///
128
  /// `nil` for client-side errors.
129
  public let underlyingData: Data?
130

131
  /// Creates a ``StorageError``.
132
  public init(
133
    message: String,
134
    errorCode: StorageErrorCode,
135
    statusCode: Int? = nil,
136
    underlyingResponse: HTTPURLResponse? = nil,
137
    underlyingData: Data? = nil
138
  ) {
26✔
139
    self.message = message
26✔
140
    self.errorCode = errorCode
26✔
141
    self.statusCode = statusCode
26✔
142
    self.underlyingResponse = underlyingResponse
26✔
143
    self.underlyingData = underlyingData
26✔
144
  }
26✔
145
}
146

147
extension StorageError {
148
  /// `true` when the error indicates that the requested object or bucket does not exist.
149
  ///
150
  /// Covers HTTP status code 404 and the explicit error codes
151
  /// ``StorageErrorCode/objectNotFound`` and ``StorageErrorCode/bucketNotFound``.
152
  public var isNotFound: Bool {
7✔
153
    statusCode == 404
7✔
154
      || errorCode == .objectNotFound
7✔
155
      || errorCode == .bucketNotFound
7✔
156
  }
7✔
157

158
  /// `true` when the error indicates an authentication or authorisation failure (status 401 or 403).
159
  public var isUnauthorized: Bool {
3✔
160
    statusCode == 401 || statusCode == 403
3✔
161
  }
3✔
162
}
163

164
extension StorageError {
165
  /// Thrown when the signed upload URL returned by the server contains no upload token.
166
  public static let noTokenReturned = StorageError(
167
    message: "No token returned by API",
168
    errorCode: .noTokenReturned
169
  )
170
}
171

172
extension StorageError: LocalizedError {
173
  public var errorDescription: String? {
1✔
174
    message
1✔
175
  }
1✔
176
}
177

178
extension StorageErrorCode {
179
  // MARK: - Transfer errors (client-side)
180

181
  /// A network error occurred during a transfer (transient; retriable on resume).
182
  public static let networkError = StorageErrorCode("NetworkError")
183
  /// A file system operation (move or read) failed during a transfer.
184
  public static let fileSystemError = StorageErrorCode("FileSystemError")
185
  /// The transfer was explicitly cancelled or the enclosing Swift Task was cancelled.
186
  public static let cancelled = StorageErrorCode("Cancelled")
187
}
188

189
extension StorageError {
190
  static func networkError(underlying: any Error) -> StorageError {
4✔
191
    StorageError(message: underlying.localizedDescription, errorCode: .networkError)
4✔
192
  }
4✔
193

194
  static func fileSystemError(underlying: any Error) -> StorageError {
1✔
195
    StorageError(message: underlying.localizedDescription, errorCode: .fileSystemError)
1✔
196
  }
1✔
197

198
  static let cancelled = StorageError(
199
    message: "Transfer was cancelled",
200
    errorCode: .cancelled
201
  )
202

203
  /// Parses a Storage server error response from raw HTTP response data.
204
  ///
205
  /// Tries to decode the Supabase Storage JSON error envelope
206
  /// `{ "message": "…", "error": "ErrorCode", "statusCode": "404" }`.
207
  /// Falls back to the raw body string and `StorageErrorCode.unknown` when decoding fails.
NEW
208
  static func from(httpResponse: HTTPURLResponse, data: Data) -> StorageError {
×
NEW
209
    struct Body: Decodable {
×
NEW
210
      let message: String?
×
NEW
211
      let error: String?
×
NEW
212
      /// The server sends the status code as a JSON string, e.g. `"404"`.
×
NEW
213
      let statusCode: String?
×
NEW
214
    }
×
NEW
215
    let decoded = try? JSONDecoder().decode(Body.self, from: data)
×
NEW
216
    return StorageError(
×
NEW
217
      message: decoded?.message ?? decoded?.error ?? String(data: data, encoding: .utf8)
×
NEW
218
        ?? "Unknown error",
×
NEW
219
      errorCode: decoded?.error.map(StorageErrorCode.init(_:)) ?? .unknown,
×
NEW
220
      statusCode: decoded?.statusCode.flatMap(Int.init) ?? httpResponse.statusCode,
×
NEW
221
      underlyingResponse: httpResponse,
×
NEW
222
      underlyingData: data
×
NEW
223
    )
×
NEW
224
  }
×
225

226
  /// Converts any `Error` to a `StorageError`.
227
  ///
228
  /// - Returns `self` when `error` is already a `StorageError`.
229
  /// - Returns ``StorageError/cancelled`` when `error` is a `CancellationError` or
230
  ///   a `URLError` with code `.cancelled`.
231
  /// - Otherwise wraps `error` as ``StorageError/networkError(underlying:)``.
232
  static func from(_ error: any Error) -> StorageError {
1✔
233
    if let storageError = error as? StorageError { return storageError }
1✔
NEW
234
    if error is CancellationError || (error as? URLError)?.code == .cancelled {
×
NEW
235
      return .cancelled
×
NEW
236
    }
×
NEW
237
    return .networkError(underlying: error)
×
238
  }
1✔
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc