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

supabase / supabase-swift / 28167379888

25 Jun 2026 11:38AM UTC coverage: 80.743%. First build
28167379888

Pull #987

github

web-flow
Merge 693c0012d into c08ceb649
Pull Request #987: feat(storage)!: storage v3

1784 of 2046 new or added lines in 14 files covered. (87.19%)

8264 of 10235 relevant lines covered (80.74%)

34.21 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