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

supabase / supabase-swift / 23865678658

01 Apr 2026 07:00PM UTC coverage: 75.937% (-2.8%) from 78.695%
23865678658

Pull #943

github

web-flow
Merge b189b75a6 into 4209dde47
Pull Request #943: feat(functions): migrate FunctionsClient to _HTTPClient

4 of 125 new or added lines in 4 files covered. (3.2%)

135 existing lines in 8 files now uncovered.

6081 of 8008 relevant lines covered (75.94%)

27.95 hits per line

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

0.0
/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 {
25
  case encodable(
26
    @autoclosure @Sendable () -> any Encodable,
27
    encoder: JSONEncoder = JSONEncoder.supabase()
28
  )
29
  case json(@autoclosure @Sendable () -> [String: Any])
30
  case data(Data)
31
}
32

33
package typealias TokenProvider = @Sendable () async throws -> String?
34

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

41
  /// The base URL for the API. This will be used as the base for all requests made by this client.
42
  package let host: URL
43

44
  /// The URLSession used to perform network requests.
45
  package let session: URLSession
46

47
  let tokenProvider: TokenProvider?
48

49
  /// The JSONDecoder used to decode responses from the server.
NEW
50
  package let jsonDecoder = JSONDecoder.supabase()
×
51

52
  package init(
53
    host: URL, session: URLSession = URLSession(configuration: .default),
54
    tokenProvider: TokenProvider? = nil
55
  ) {
×
56
    self.host = host
×
57
    self.session = session
×
58
    self.tokenProvider = tokenProvider
×
59
  }
×
60

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

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

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

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

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

119
    /// Streams the response body from an absolute `url` byte-by-byte.
120
    ///
121
    /// - Note: Only available on Apple platforms. `URLSession.bytes(for:)` is not available on Linux.
122
    @available(macOS 12.0, *)
123
    package func fetchStream(
124
      _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
125
      headers: [String: String]? = nil
NEW
126
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
×
NEW
127
      try await performFetchStream(
×
128
        method,
×
129
        requestBuilder: { [self] in
×
130
          try await self.createRequest(method, url: url, query: query, body: body, headers: headers)
×
131
        }
×
132
      )
×
133
    }
×
134

135
    @available(macOS 12.0, *)
136
    private func performFetchStream(
137
      _ method: HTTPMethod, requestBuilder: @escaping @Sendable () async throws -> URLRequest
NEW
138
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
×
NEW
139
      let request = try await requestBuilder()
×
NEW
140

×
NEW
141
      let (bytes, response) = try await session.bytes(for: request)
×
NEW
142
      let httpResponse = try validateResponse(response)
×
NEW
143

×
NEW
144
      guard (200..<300).contains(httpResponse.statusCode) else {
×
NEW
145
        var errorData = Data()
×
NEW
146
        for try await byte in bytes {
×
NEW
147
          errorData.append(byte)
×
148
        }
×
NEW
149
        _ = try validateResponse(response, data: errorData)
×
NEW
150
        return (.finished(), httpResponse)
×
NEW
151
      }
×
152

×
NEW
153
      let (stream, continuation) = AsyncThrowingStream<UInt8, any Error>.makeStream()
×
NEW
154

×
NEW
155
      let task = Task {
×
NEW
156
        for try await byte in bytes {
×
NEW
157
          continuation.yield(byte)
×
158
        }
×
NEW
159
        continuation.finish()
×
160
      }
×
NEW
161

×
NEW
162
      continuation.onTermination = { _ in task.cancel() }
×
NEW
163

×
NEW
164
      return (stream, httpResponse)
×
UNCOV
165
    }
×
166
  #endif
167

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

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

187
  private func performFetch(
188
    request: URLRequest
189
  ) async throws -> (Data, HTTPURLResponse) {
×
190
    let (data, response) = try await session.data(for: request)
×
191
    let httpResponse = try validateResponse(response, data: data)
×
192
    return (data, httpResponse)
×
193
  }
×
194

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

×
210
    return try await createRequest(
×
211
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
×
212
  }
×
213

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

×
221
    return try await createRequest(
×
222
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
×
223
  }
×
224

225
  private func createRequest(
226
    _ method: HTTPMethod, urlComponents: URLComponents?, query: [String: String]? = nil,
227
    body: RequestBody? = nil, headers: [String: String]? = nil
228
  ) async throws -> URLRequest {
×
229
    var urlComponents = urlComponents
×
230

×
231
    if let query {
×
232
      var queryItems = urlComponents?.queryItems ?? []
×
233
      for (key, value) in query {
×
234
        queryItems.append(URLQueryItem(name: key, value: value))
×
235
      }
×
236
      urlComponents?.queryItems = queryItems
×
237
    }
×
238

×
239
    guard let url = urlComponents?.url else {
×
240
      throw URLError(.badURL)
×
241
    }
×
242

×
243
    var request = URLRequest(url: url)
×
244
    request.httpMethod = method.rawValue
×
245

×
246
    if let headers {
×
247
      for (key, value) in headers {
×
248
        request.setValue(value, forHTTPHeaderField: key)
×
249
      }
×
250
    }
×
251

×
252
    if let tokenProvider,
×
253
      request.value(forHTTPHeaderField: "Authorization") == nil,
×
254
      let token = try await tokenProvider()
×
255
    {
×
256
      request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
×
257
    }
×
258

×
259
    if request.value(forHTTPHeaderField: "Accept") == nil {
×
260
      request.setValue("application/json", forHTTPHeaderField: "Accept")
×
261
    }
×
262

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

×
277
    return request
×
278
  }
×
279

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

×
295
    if let data, !(200..<300).contains(response.statusCode) {
×
296
      throw HTTPClientError.responseError(response, data: data)
×
297
    }
×
298

×
299
    return response
×
300
  }
×
301
}
302

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