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

supabase / supabase-swift / 25046488840

28 Apr 2026 10:00AM UTC coverage: 80.858% (+1.1%) from 79.762%
25046488840

Pull #923

github

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

125 of 142 new or added lines in 5 files covered. (88.03%)

1 existing line in 1 file now uncovered.

7198 of 8902 relevant lines covered (80.86%)

31.75 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
/// HTTP methods supported by ``_HTTPClient``.
11
package enum HTTPMethod: String {
12
  case get = "GET"
13
  case head = "HEAD"
14
  case post = "POST"
15
  case patch = "PATCH"
16
  case put = "PUT"
17
  case delete = "DELETE"
18
}
19

20
package enum RequestBody: @unchecked Sendable {
21
  case encodable(any Encodable, encoder: JSONEncoder = JSONEncoder.supabase())
22
  case json([String: Any])
23
  case data(Data)
24
}
25

26
package typealias TokenProvider = @Sendable () async throws -> String?
27

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

34
  /// The base URL for the API. This will be used as the base for all requests made by this client.
35
  package let host: URL
36

37
  /// The URLSession used to perform network requests.
38
  package let session: URLSession
39

40
  let tokenProvider: TokenProvider?
41

42
  /// The JSONDecoder used to decode responses from the server.
43
  package let jsonDecoder = JSONDecoder.supabase()
36✔
44

45
  package init(
46
    host: URL, session: URLSession = URLSession(configuration: .default),
47
    tokenProvider: TokenProvider? = nil
48
  ) {
36✔
49
    self.host = host
36✔
50
    self.session = session
36✔
51
    self.tokenProvider = tokenProvider
36✔
52
  }
36✔
53

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

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

79
  private func performFetch<T: Decodable>(
80
    request: URLRequest
81
  ) async throws -> (T, HTTPURLResponse) {
×
82
    let (data, response) = try await performFetch(request: request)
×
83

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

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

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

123
  @available(macOS 12.0, *)
124
  private func performFetchStream(
125
    _ method: HTTPMethod, requestBuilder: @escaping @Sendable () async throws -> URLRequest
126
  ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
3✔
127
    let request = try await requestBuilder()
3✔
128

3✔
129
    let (bytes, response) = try await session.bytes(for: request)
3✔
130
    let httpResponse = try validateResponse(response)
3✔
131

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

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

2✔
144
    let task = Task {
2✔
145
      for try await byte in bytes {
11✔
146
        continuation.yield(byte)
11✔
147
      }
11✔
148

2✔
149
      continuation.finish()
2✔
150
    }
2✔
151

2✔
152
    continuation.onTermination = { _ in
2✔
153
      task.cancel()
2✔
154
    }
2✔
155

2✔
156
    return (stream, httpResponse)
2✔
157
  }
3✔
158

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

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

178
  private func performFetch(
179
    request: URLRequest
180
  ) async throws -> (Data, HTTPURLResponse) {
29✔
181
    let (data, response) = try await session.data(for: request)
29✔
182
    let httpResponse = try validateResponse(response, data: data)
28✔
183
    return (data, httpResponse)
27✔
184
  }
29✔
185

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

×
201
    return try await createRequest(
×
202
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
×
203
  }
×
204

205
  /// Builds a `URLRequest` from an absolute `url`.
206
  package func createRequest(
207
    _ method: HTTPMethod, url: URL, query: [String: String]? = nil, body: RequestBody? = nil,
208
    headers: [String: String]? = nil
209
  ) async throws -> URLRequest {
32✔
210
    let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)
32✔
211

32✔
212
    return try await createRequest(
32✔
213
      method, urlComponents: urlComponents, query: query, body: body, headers: headers)
32✔
214
  }
32✔
215

216
  private func createRequest(
217
    _ method: HTTPMethod, urlComponents: URLComponents?, query: [String: String]? = nil,
218
    body: RequestBody? = nil, headers: [String: String]? = nil
219
  ) async throws -> URLRequest {
32✔
220
    var urlComponents = urlComponents
32✔
221

32✔
222
    if let query {
32✔
223
      var queryItems = urlComponents?.queryItems ?? []
6✔
224
      for (key, value) in query {
6✔
225
        queryItems.append(URLQueryItem(name: key, value: value))
6✔
226
      }
6✔
227
      urlComponents?.queryItems = queryItems
6✔
228
    }
6✔
229

32✔
230
    guard let url = urlComponents?.url else {
32✔
231
      throw URLError(.badURL)
×
232
    }
32✔
233

32✔
234
    var request = URLRequest(url: url)
32✔
235
    request.httpMethod = method.rawValue
32✔
236

32✔
237
    if let headers {
32✔
238
      for (key, value) in headers {
81✔
239
        request.setValue(value, forHTTPHeaderField: key)
81✔
240
      }
81✔
241
    }
32✔
242

32✔
243
    if let tokenProvider,
32✔
244
      request.value(forHTTPHeaderField: "Authorization") == nil,
32✔
245
      let token = try await tokenProvider()
32✔
246
    {
32✔
247
      request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
×
248
    }
×
249

32✔
250
    if request.value(forHTTPHeaderField: "Accept") == nil {
32✔
251
      request.setValue("application/json", forHTTPHeaderField: "Accept")
32✔
252
    }
32✔
253

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

32✔
268
    return request
32✔
269
  }
32✔
270

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

32✔
286
    if let data, !(200..<300).contains(response.statusCode) {
32✔
287
      throw HTTPClientError.responseError(response, data: data)
2✔
288
    }
30✔
289

30✔
290
    return response
30✔
291
  }
32✔
292
}
293

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