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

supabase / supabase-swift / 25050382831

28 Apr 2026 11:33AM UTC coverage: 81.083% (+1.3%) from 79.762%
25050382831

Pull #923

github

web-flow
Merge 206fbad7f into aef63d63c
Pull Request #923: feat(functions): functions v3

121 of 132 new or added lines in 5 files covered. (91.67%)

7 existing lines in 3 files now uncovered.

7218 of 8902 relevant lines covered (81.08%)

31.73 hits per line

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

70.73
/Sources/Helpers/_HTTPClient.swift
1
//
2
//  _HTTPClient.swift
3
//  Supabase
4
//
5
//  Created by Guilherme Souza on 12/03/26.
6
//
7

8
import Foundation
9

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

14
/// HTTP methods supported by ``_HTTPClient``.
15
package enum HTTPMethod: String {
16
  case get = "GET"
17
  case head = "HEAD"
18
  case post = "POST"
19
  case patch = "PATCH"
20
  case put = "PUT"
21
  case delete = "DELETE"
22
}
23

24
package enum RequestBody: @unchecked Sendable {
25
  case encodable(any Encodable, encoder: JSONEncoder = JSONEncoder.supabase())
26
  case json([String: Any])
27
  case data(Data)
28
}
29

30
package typealias TokenProvider = @Sendable () async throws -> String?
31

32
/// HTTP client for making all Supabase API requests.
33
///
34
/// Builds `URLRequest` values from a base `host` URL and dispatches them via `URLSession`.
35
/// Responses are validated for 2xx status codes before being returned.
36
package final class _HTTPClient: Sendable {
37

38
  /// The base URL for the API. This will be used as the base for all requests made by this client.
39
  package let host: URL
40

41
  /// The URLSession used to perform network requests.
42
  package let session: URLSession
43

44
  let tokenProvider: TokenProvider?
45

46
  /// The JSONDecoder used to decode responses from the server.
47
  package let jsonDecoder = JSONDecoder.supabase()
36✔
48

49
  package init(
50
    host: URL, session: URLSession = URLSession(configuration: .default),
51
    tokenProvider: TokenProvider? = nil
52
  ) {
36✔
53
    self.host = host
36✔
54
    self.session = session
36✔
55
    self.tokenProvider = tokenProvider
36✔
56
  }
36✔
57

58
  /// Performs a request relative to ``host``, decoding the response body as `T`.
59
  ///
60
  /// Uses ``jsonDecoder`` (a `JSONDecoder` with Supabase defaults) to decode the response.
61
  /// If you need custom decoding, use ``fetchData(_:_:query:body:headers:)`` and decode at the call site.
62
  package func fetch<T: Decodable>(
63
    _ method: HTTPMethod, _ path: String, query: [String: String]? = nil,
64
    body: RequestBody? = nil, headers: [String: String]? = nil
65
  ) async throws -> (T, HTTPURLResponse) {
×
66
    let request = try await createRequest(method, path, query: query, body: body, headers: headers)
×
67
    return try await performFetch(request: request)
×
68
  }
×
69

70
  /// Performs a request to an absolute `url`, decoding the response body as `T`.
71
  ///
72
  /// Uses ``jsonDecoder`` (a `JSONDecoder` with Supabase defaults) to decode the response.
73
  /// If you need custom decoding, use ``fetchData(_:url:query:body:headers:)`` and decode at the call site.
74
  package func fetch<T: Decodable>(
75
    _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
76
    headers: [String: String]? = nil
77
  ) async throws -> (T, HTTPURLResponse) {
×
78
    let request = try await createRequest(
×
79
      method, url: url, query: query, body: body, headers: headers)
×
80
    return try await performFetch(request: request)
×
81
  }
×
82

83
  private func performFetch<T: Decodable>(
84
    request: URLRequest
85
  ) async throws -> (T, HTTPURLResponse) {
×
86
    let (data, response) = try await performFetch(request: request)
×
87

×
88
    do {
×
89
      let value = try jsonDecoder.decode(T.self, from: data)
×
90
      return (value, response)
×
91
    } catch {
×
92
      throw HTTPClientError.decodingError(response, detail: error.localizedDescription)
×
93
    }
×
94
  }
×
95

96
  #if canImport(Darwin)
97
    /// Streams the response body byte-by-byte via an `AsyncThrowingStream`.
98
    ///
99
    /// Cancelling the stream cancels the underlying `URLSession` task. Non-2xx responses
100
    /// buffer the full error body and throw ``HTTPClientError/responseError(_:data:)``.
101
    @available(macOS 12.0, *)
102
    package func fetchStream(
103
      _ method: HTTPMethod, _ path: String, query: [String: String]? = nil,
104
      body: RequestBody? = nil, headers: [String: String]? = nil
NEW
105
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
×
NEW
106
      try await performFetchStream(
×
107
        method,
×
108
        requestBuilder: { [self] in
×
109
          try await self.createRequest(method, path, query: query, body: body, headers: headers)
×
110
        }
×
111
      )
×
112
    }
×
113

114
    /// Streams the response body from an absolute `url` byte-by-byte.
115
    @available(macOS 12.0, *)
116
    package func fetchStream(
117
      _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
118
      headers: [String: String]? = nil
119
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
3✔
120
      try await performFetchStream(
3✔
121
        method,
3✔
122
        requestBuilder: { [self] in
3✔
123
          try await self.createRequest(method, url: url, query: query, body: body, headers: headers)
3✔
124
        }
3✔
125
      )
3✔
126
    }
2✔
127

128
    @available(macOS 12.0, *)
129
    private func performFetchStream(
130
      _ method: HTTPMethod, requestBuilder: @escaping @Sendable () async throws -> URLRequest
131
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
3✔
132
      let request = try await requestBuilder()
3✔
133

3✔
134
      let (bytes, response) = try await session.bytes(for: request)
3✔
135
      let httpResponse = try validateResponse(response)
3✔
136

3✔
137
      guard (200..<300).contains(httpResponse.statusCode) else {
3✔
138
        var errorData = Data()
1✔
139
        for try await byte in bytes {
1✔
NEW
140
          errorData.append(byte)
×
141
        }
1✔
142
        // validateResponse will throw the appropriate error
1✔
143
        _ = try validateResponse(response, data: errorData)
1✔
NEW
144
        return (.finished(), httpResponse)  // This will never be reached, but is needed to satisfy the return type
×
145
      }
2✔
146

2✔
147
      let (stream, continuation) = AsyncThrowingStream<UInt8, any Error>.makeStream()
2✔
148

2✔
149
      let task = Task {
2✔
150
        for try await byte in bytes {
11✔
151
          continuation.yield(byte)
11✔
152
        }
11✔
153

1✔
154
        continuation.finish()
1✔
155
      }
1✔
156

2✔
157
      continuation.onTermination = { _ in
2✔
158
        task.cancel()
2✔
159
      }
2✔
160

2✔
161
      return (stream, httpResponse)
2✔
162
    }
3✔
163
  #endif
164

165
  /// Performs a request relative to ``host``, returning the raw response body.
166
  package func fetchData(
167
    _ method: HTTPMethod, _ path: String, query: [String: String]? = nil,
168
    body: RequestBody? = nil, headers: [String: String]? = nil
169
  ) async throws -> (Data, HTTPURLResponse) {
×
170
    let request = try await createRequest(method, path, query: query, body: body, headers: headers)
×
171
    return try await performFetch(request: request)
×
172
  }
×
173

174
  /// Performs a request to an absolute `url`, returning the raw response body.
175
  package func fetchData(
176
    _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
177
    headers: [String: String]? = nil
178
  ) async throws -> (Data, HTTPURLResponse) {
29✔
179
    let request = try await createRequest(
29✔
180
      method, url: url, query: query, body: body, headers: headers)
29✔
181
    return try await performFetch(request: request)
29✔
182
  }
29✔
183

184
  private func performFetch(
185
    request: URLRequest
186
  ) async throws -> (Data, HTTPURLResponse) {
29✔
187
    let (data, response) = try await session.data(for: request)
29✔
188
    let httpResponse = try validateResponse(response, data: data)
28✔
189
    return (data, httpResponse)
27✔
190
  }
29✔
191

192
  /// Builds a `URLRequest` by appending `path` to ``host``.
193
  ///
194
  /// - Parameters:
195
  ///   - method: The HTTP method.
196
  ///   - path: The path component to append to ``host``.
197
  ///   - query: Optional query parameters. Always encoded as URL query items.
198
  ///   - body: Optional request body. Encoded according to the ``RequestBody`` case.
199
  ///   - headers: Optional additional headers. `Accept: application/json` is added by default.
200
  package func createRequest(
201
    _ method: HTTPMethod, _ path: String, query: [String: String]? = nil,
202
    body: RequestBody? = nil, headers: [String: String]? = nil
203
  ) async throws -> URLRequest {
×
204
    var urlComponents = URLComponents(url: host, resolvingAgainstBaseURL: true)
×
205
    urlComponents?.path = path
×
206

×
207
    return try await createRequest(
×
208
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
×
209
  }
×
210

211
  /// Builds a `URLRequest` from an absolute `url`.
212
  package func createRequest(
213
    _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
214
    headers: [String: String]? = nil
215
  ) async throws -> URLRequest {
32✔
216
    let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)
32✔
217

32✔
218
    return try await createRequest(
32✔
219
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
32✔
220
  }
32✔
221

222
  private func createRequest(
223
    _ method: HTTPMethod, urlComponents: URLComponents?, query: [String: String]? = nil,
224
    body: RequestBody? = nil, headers: [String: String]? = nil
225
  ) async throws -> URLRequest {
32✔
226
    var urlComponents = urlComponents
32✔
227

32✔
228
    if let query {
32✔
229
      var queryItems = urlComponents?.queryItems ?? []
6✔
230
      for (key, value) in query {
6✔
231
        queryItems.append(URLQueryItem(name: key, value: value))
6✔
232
      }
6✔
233
      urlComponents?.queryItems = queryItems
6✔
234
    }
6✔
235

32✔
236
    guard let url = urlComponents?.url else {
32✔
237
      throw URLError(.badURL)
×
238
    }
32✔
239

32✔
240
    var request = URLRequest(url: url)
32✔
241
    request.httpMethod = method.rawValue
32✔
242

32✔
243
    if let headers {
32✔
244
      for (key, value) in headers {
81✔
245
        request.setValue(value, forHTTPHeaderField: key)
81✔
246
      }
81✔
247
    }
32✔
248

32✔
249
    if let tokenProvider,
32✔
250
      request.value(forHTTPHeaderField: "Authorization") == nil,
32✔
251
      let token = try await tokenProvider()
32✔
252
    {
32✔
253
      request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
×
254
    }
×
255

32✔
256
    if request.value(forHTTPHeaderField: "Accept") == nil {
32✔
257
      request.setValue("application/json", forHTTPHeaderField: "Accept")
32✔
258
    }
32✔
259

32✔
260
    if let body {
32✔
261
      switch body {
11✔
262
      case .encodable(let value, let encoder):
11✔
NEW
263
        request.httpBody = try encoder.encode(value)
×
264
      case .json(let dict):
11✔
NEW
265
        request.httpBody = try JSONSerialization.data(withJSONObject: dict)
×
266
      case .data(let data):
11✔
267
        request.httpBody = data
11✔
268
      }
11✔
269
      if request.value(forHTTPHeaderField: "Content-Type") == nil {
11✔
270
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
2✔
271
      }
2✔
272
    }
32✔
273

32✔
274
    return request
32✔
275
  }
32✔
276

277
  /// Casts `response` to `HTTPURLResponse` and throws if the status code is outside 2xx.
278
  ///
279
  /// - Parameters:
280
  ///   - response: The raw `URLResponse` to validate.
281
  ///   - data: When provided, included in ``HTTPClientError/responseError(_:data:)`` on failure.
282
  @discardableResult
283
  package func validateResponse(_ response: URLResponse, data: Data? = nil) throws
284
    -> HTTPURLResponse
285
  {
32✔
286
    guard let response = response as? HTTPURLResponse else {
32✔
287
      throw HTTPClientError.unexpectedError(
×
288
        "Invalid response from server: \(response)"
×
289
      )
×
290
    }
32✔
291

32✔
292
    if let data, !(200..<300).contains(response.statusCode) {
32✔
293
      throw HTTPClientError.responseError(response, data: data)
2✔
294
    }
30✔
295

30✔
296
    return response
30✔
297
  }
32✔
298
}
299

300
/// Errors thrown by ``_HTTPClient``.
301
package enum HTTPClientError: Error {
302
  /// The server returned a non-2xx status code. The raw response body is included in `data`.
303
  case responseError(HTTPURLResponse, data: Data)
304
  /// The response body could not be decoded into the expected type.
305
  case decodingError(HTTPURLResponse, detail: String)
306
  /// An unexpected error occurred (e.g. the response was not an `HTTPURLResponse`).
307
  case unexpectedError(String)
308
}
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