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

supabase / supabase-swift / 28785872053

06 Jul 2026 10:44AM UTC coverage: 80.82% (-0.1%) from 80.957%
28785872053

push

github

web-flow
feat: raise minimum platform versions to iOS 16 and equivalents (#1067)

Bumps Package.swift platform minimums to iOS 16, macCatalyst 16,
macOS 13 (Ventura), watchOS 9, and tvOS 16 — the versions that shipped
together in the iOS 16 generation. visionOS stays at 1.0+ since its
first release already postdates iOS 16. Updates README.md/AGENTS.md
to match.

Stacked on top of the Swift 6.1 minimum-version bump.

7652 of 9468 relevant lines covered (80.82%)

37.0 hits per line

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

97.09
/Sources/Functions/FunctionsClient.swift
1
import ConcurrencyExtras
2
import Foundation
3
import HTTPTypes
4
import Helpers
5

6
#if canImport(FoundationNetworking)
7
  import FoundationNetworking
8
#endif
9

10
let version = Helpers.version
11

12
/// A client for invoking Supabase Edge Functions.
13
///
14
/// Obtain an instance from ``SupabaseClient/functions`` rather than creating one directly.
15
///
16
/// ```swift
17
/// // Invoke and decode a response
18
/// let order: Order = try await supabase.functions.invoke("get-order")
19
///
20
/// // Invoke with a body and no return value
21
/// try await supabase.functions.invoke(
22
///   "send-email",
23
///   options: FunctionInvokeOptions(body: ["to": "user@example.com"])
24
/// )
25
/// ```
26
///
27
/// ## Topics
28
///
29
/// ### Creating a Client
30
/// - ``init(url:headers:region:logger:fetch:decoder:)``
31
/// - ``FetchHandler``
32
///
33
/// ### Invoking Functions
34
/// - ``invoke(_:options:decode:)``
35
/// - ``invoke(_:options:decoder:)``
36
/// - ``invoke(_:options:)``
37
/// - ``_invokeWithStreamedResponse(_:options:)``
38
///
39
/// ### Configuration
40
/// - ``decoder``
41
/// - ``requestIdleTimeout``
42
/// - ``setAuth(token:)``
43
public final class FunctionsClient: Sendable {
44
  /// A handler that performs the underlying HTTP request for a function invocation.
45
  public typealias FetchHandler =
46
    @Sendable (_ request: URLRequest) async throws -> (
47
      Data, URLResponse
48
    )
49

50
  /// The maximum time an Edge Function may run before the gateway returns a 504 error (150 seconds).
51
  public static let requestIdleTimeout: TimeInterval = 150
52

53
  /// The base URL for the functions.
54
  let url: URL
55

56
  /// The Region to invoke the functions in.
57
  let region: String?
58

59
  /// The JSON decoder used to decode function response bodies.
60
  public let decoder: JSONDecoder
61

62
  struct MutableState {
63
    /// Headers to be included in the requests.
64
    var headers = HTTPFields()
22✔
65
  }
66

67
  private let http: any HTTPClientType
68
  private let mutableState = LockIsolated(MutableState())
22✔
69
  private let sessionConfiguration: URLSessionConfiguration
70

71
  var headers: HTTPFields {
5✔
72
    mutableState.headers
5✔
73
  }
5✔
74

75
  /// Creates a new Functions client.
76
  /// - Parameters:
77
  ///   - url: The base URL of the Functions endpoint.
78
  ///   - headers: Additional headers to include in every request.
79
  ///   - region: The region string to invoke functions in.
80
  ///   - logger: A logger for request and response diagnostics.
81
  ///   - fetch: A custom fetch handler. Defaults to `URLSession.shared`.
82
  ///   - decoder: The JSON decoder used to decode response bodies.
83
  @_disfavoredOverload
84
  public convenience init(
85
    url: URL,
86
    headers: [String: String] = [:],
87
    region: String? = nil,
88
    logger: (any SupabaseLogger)? = nil,
89
    fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) },
×
90
    decoder: JSONDecoder = JSONDecoder()
91
  ) {
8✔
92
    self.init(
8✔
93
      url: url,
8✔
94
      headers: headers,
8✔
95
      region: region,
8✔
96
      logger: logger,
8✔
97
      fetch: fetch,
8✔
98
      decoder: decoder,
8✔
99
      sessionConfiguration: .default
8✔
100
    )
8✔
101
  }
8✔
102

103
  convenience init(
104
    url: URL,
105
    headers: [String: String] = [:],
106
    region: String? = nil,
107
    logger: (any SupabaseLogger)? = nil,
108
    fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) },
×
109
    decoder: JSONDecoder = JSONDecoder(),
110
    sessionConfiguration: URLSessionConfiguration
111
  ) {
22✔
112
    var interceptors: [any HTTPClientInterceptor] = []
22✔
113
    if let logger {
22✔
114
      interceptors.append(LoggerInterceptor(logger: logger))
1✔
115
    }
1✔
116

22✔
117
    let http = HTTPClient(fetch: fetch, interceptors: interceptors)
22✔
118

22✔
119
    self.init(
22✔
120
      url: url,
22✔
121
      headers: headers,
22✔
122
      region: region,
22✔
123
      decoder: decoder,
22✔
124
      http: http,
22✔
125
      sessionConfiguration: sessionConfiguration
22✔
126
    )
22✔
127
  }
22✔
128

129
  init(
130
    url: URL,
131
    headers: [String: String],
132
    region: String?,
133
    decoder: JSONDecoder = JSONDecoder(),
134
    http: any HTTPClientType,
135
    sessionConfiguration: URLSessionConfiguration = .default
136
  ) {
22✔
137
    self.url = url
22✔
138
    self.region = region
22✔
139
    self.decoder = decoder
22✔
140
    self.http = http
22✔
141
    self.sessionConfiguration = sessionConfiguration
22✔
142

22✔
143
    mutableState.withValue {
22✔
144
      $0.headers = HTTPFields(headers)
22✔
145
      if $0.headers[.xClientInfo] == nil {
22✔
146
        $0.headers[.xClientInfo] = "functions-swift/\(version)"
16✔
147
      }
16✔
148
    }
22✔
149
  }
22✔
150

151
  /// Creates a new Functions client.
152
  /// - Parameters:
153
  ///   - url: The base URL of the Functions endpoint.
154
  ///   - headers: Additional headers to include in every request.
155
  ///   - region: The region to invoke functions in.
156
  ///   - logger: A logger for request and response diagnostics.
157
  ///   - fetch: A custom fetch handler. Defaults to `URLSession.shared`.
158
  ///   - decoder: The JSON decoder used to decode response bodies.
159
  public convenience init(
160
    url: URL,
161
    headers: [String: String] = [:],
162
    region: FunctionRegion? = nil,
163
    logger: (any SupabaseLogger)? = nil,
164
    fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) },
×
165
    decoder: JSONDecoder = JSONDecoder()
166
  ) {
7✔
167
    self.init(
7✔
168
      url: url,
7✔
169
      headers: headers,
7✔
170
      region: region?.rawValue,
7✔
171
      logger: logger,
7✔
172
      fetch: fetch,
7✔
173
      decoder: decoder
7✔
174
    )
7✔
175
  }
7✔
176

177
  /// Sets or clears the JWT used in the Authorization header for subsequent requests.
178
  /// - Parameter token: The JWT to send, or `nil` to remove the Authorization header.
179
  public func setAuth(token: String?) {
2✔
180
    mutableState.withValue {
2✔
181
      if let token {
2✔
182
        $0.headers[.authorization] = "Bearer \(token)"
1✔
183
      } else {
1✔
184
        $0.headers[.authorization] = nil
1✔
185
      }
1✔
186
    }
2✔
187
  }
2✔
188

189
  /// Invokes a function and decodes the response with a custom closure.
190
  /// - Parameters:
191
  ///   - functionName: The name of the function to invoke.
192
  ///   - options: Options for the invocation.
193
  ///   - decode: A closure that receives the raw response data and HTTP response, and returns the
194
  ///     decoded value.
195
  /// - Returns: The value returned by `decode`.
196
  /// - Throws: ``FunctionsError`` if the function returns a non-2xx status or a relay error.
197
  public func invoke<Response>(
198
    _ functionName: String,
199
    options: FunctionInvokeOptions = .init(),
200
    decode: (Data, HTTPURLResponse) throws -> Response
201
  ) async throws -> Response {
15✔
202
    let response = try await rawInvoke(
15✔
203
      functionName: functionName, invokeOptions: options
15✔
204
    )
15✔
205
    return try decode(response.data, response.underlyingResponse)
7✔
206
  }
15✔
207

208
  /// Invokes a function and JSON-decodes the response body into `T`.
209
  /// - Parameters:
210
  ///   - functionName: The name of the function to invoke.
211
  ///   - options: Options for the invocation.
212
  ///   - decoder: The JSON decoder to use. Defaults to the client's ``decoder`` when `nil`.
213
  /// - Returns: The decoded `T`.
214
  /// - Throws: ``FunctionsError`` if the function returns a non-2xx status or a relay error, or
215
  ///   a decoding error if the response body cannot be decoded as `T`.
216
  public func invoke<T: Decodable>(
217
    _ functionName: String,
218
    options: FunctionInvokeOptions = .init(),
219
    decoder: JSONDecoder? = nil
220
  ) async throws -> T {
1✔
221
    let decoder = decoder ?? self.decoder
1✔
222
    return try await invoke(functionName, options: options) { data, _ in
1✔
223
      try decoder.decode(T.self, from: data)
1✔
224
    }
1✔
225
  }
1✔
226

227
  /// Invokes a function and discards any response body.
228
  /// - Parameters:
229
  ///   - functionName: The name of the function to invoke.
230
  ///   - options: Options for the invocation.
231
  /// - Throws: ``FunctionsError`` if the function returns a non-2xx status or a relay error.
232
  public func invoke(
233
    _ functionName: String,
234
    options: FunctionInvokeOptions = .init()
235
  ) async throws {
14✔
236
    try await invoke(functionName, options: options) { _, _ in () }
14✔
237
  }
6✔
238

239
  private func rawInvoke(
240
    functionName: String,
241
    invokeOptions: FunctionInvokeOptions
242
  ) async throws -> Helpers.HTTPResponse {
15✔
243
    let request = buildRequest(functionName: functionName, options: invokeOptions)
15✔
244
    let response = try await http.send(request)
15✔
245

9✔
246
    guard 200..<300 ~= response.statusCode else {
9✔
247
      throw FunctionsError.httpError(code: response.statusCode, data: response.data)
1✔
248
    }
8✔
249

8✔
250
    let isRelayError = response.headers[.xRelayError] == "true"
8✔
251
    if isRelayError {
8✔
252
      throw FunctionsError.relayError
1✔
253
    }
7✔
254

7✔
255
    return response
7✔
256
  }
15✔
257

258
  /// Invokes a function and returns its response as a stream of raw `Data` chunks.
259
  ///
260
  /// The function must return a `text/event-stream` content type for this to work correctly.
261
  ///
262
  /// > Warning: Experimental — the API may change without a major version bump.
263
  ///
264
  /// > Note: This method uses a separate `URLSession` from the rest of the client.
265
  /// - Parameters:
266
  ///   - functionName: The name of the function to invoke.
267
  ///   - options: Options for the invocation.
268
  /// - Returns: An `AsyncThrowingStream` that yields response data chunks as they arrive.
269
  public func _invokeWithStreamedResponse(
270
    _ functionName: String,
271
    options invokeOptions: FunctionInvokeOptions = .init()
272
  ) -> AsyncThrowingStream<Data, any Error> {
3✔
273
    let (stream, continuation) = AsyncThrowingStream<Data, any Error>.makeStream()
3✔
274
    let delegate = StreamResponseDelegate(continuation: continuation)
3✔
275

3✔
276
    let session = URLSession(
3✔
277
      configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
3✔
278

3✔
279
    let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest
3✔
280

3✔
281
    let task = session.dataTask(with: urlRequest)
3✔
282
    task.resume()
3✔
283

3✔
284
    continuation.onTermination = { _ in
3✔
285
      task.cancel()
3✔
286

3✔
287
      // Hold a strong reference to delegate until continuation terminates.
3✔
288
      _ = delegate
3✔
289
    }
3✔
290

3✔
291
    return stream
3✔
292
  }
3✔
293

294
  private func buildRequest(functionName: String, options: FunctionInvokeOptions)
295
    -> Helpers.HTTPRequest
296
  {
18✔
297
    var query = options.query
18✔
298
    var request = HTTPRequest(
18✔
299
      url: url.appendingPathComponent(functionName),
18✔
300
      method: FunctionInvokeOptions.httpMethod(options.method) ?? .post,
18✔
301
      query: query,
18✔
302
      headers: mutableState.headers.merging(with: options.headers),
18✔
303
      body: options.body,
18✔
304
      timeoutInterval: FunctionsClient.requestIdleTimeout
18✔
305
    )
18✔
306

18✔
307
    if let region = options.region ?? region {
18✔
308
      request.headers[.xRegion] = region
3✔
309
      query.appendOrUpdate(URLQueryItem(name: "forceFunctionRegion", value: region))
3✔
310
      request.query = query
3✔
311
    }
3✔
312

18✔
313
    return request
18✔
314
  }
18✔
315
}
316

317
final class StreamResponseDelegate: NSObject, URLSessionDataDelegate, Sendable {
318
  let continuation: AsyncThrowingStream<Data, any Error>.Continuation
319

320
  init(continuation: AsyncThrowingStream<Data, any Error>.Continuation) {
3✔
321
    self.continuation = continuation
3✔
322
  }
3✔
323

324
  func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
1✔
325
    continuation.yield(data)
1✔
326
  }
1✔
327

328
  func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?) {
3✔
329
    continuation.finish(throwing: error)
3✔
330
  }
3✔
331

332
  func urlSession(
333
    _: URLSession, dataTask _: URLSessionDataTask, didReceive response: URLResponse,
334
    completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
335
  ) {
3✔
336
    defer {
3✔
337
      completionHandler(.allow)
3✔
338
    }
3✔
339

3✔
340
    guard let httpResponse = response as? HTTPURLResponse else {
3✔
341
      continuation.finish(throwing: URLError(.badServerResponse))
×
342
      return
×
343
    }
3✔
344

3✔
345
    guard 200..<300 ~= httpResponse.statusCode else {
3✔
346
      let error = FunctionsError.httpError(
1✔
347
        code: httpResponse.statusCode,
1✔
348
        data: Data()
1✔
349
      )
1✔
350
      continuation.finish(throwing: error)
1✔
351
      return
1✔
352
    }
2✔
353

2✔
354
    let isRelayError = httpResponse.value(forHTTPHeaderField: "x-relay-error") == "true"
2✔
355
    if isRelayError {
2✔
356
      continuation.finish(throwing: FunctionsError.relayError)
1✔
357
    }
1✔
358
  }
2✔
359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc