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

supabase / supabase-swift / 25048706637

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

Pull #923

github

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

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

1 existing line in 1 file now uncovered.

7198 of 8902 relevant lines covered (80.86%)

31.71 hits per line

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

98.29
/Sources/Functions/FunctionsClient.swift
1
import ConcurrencyExtras
2
import Foundation
3
import Helpers
4

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

9
let version = Helpers.version
10

11
/// A client for invoking Supabase Edge Functions.
12
///
13
/// `FunctionsClient` provides methods for calling Edge Functions deployed on your Supabase project.
14
/// It handles authentication token injection, region routing, and response decoding.
15
///
16
/// ## Basic usage
17
///
18
/// ```swift
19
/// let client = FunctionsClient(
20
///   url: URL(string: "https://<project-ref>.supabase.co/functions/v1")!,
21
///   headers: ["Authorization": "Bearer <anon-key>"]
22
/// )
23
///
24
/// // Invoke a function and decode the JSON response
25
/// let (result, _) = try await client.invokeDecodable("my-function", as: MyResponse.self)
26
///
27
/// // Invoke a function and handle raw data
28
/// let (data, response) = try await client.invoke("my-function") {
29
///   $0.method = .post
30
///   $0.body = try! JSONEncoder().encode(myPayload)
31
///   $0.headers["Content-Type"] = "application/json"
32
/// }
33
/// ```
34
///
35
/// When used via ``SupabaseClient``, authentication tokens are automatically refreshed and injected
36
/// into every request. You do not need to manage ``setAuth(token:)`` manually in that case.
37
public actor FunctionsClient {
38
  /// The maximum time an Edge Function may be idle before the gateway returns a 504.
39
  ///
40
  /// Supabase enforces a 150-second request idle timeout for Edge Functions. The client
41
  /// configures the underlying `URLSession` with this value so local timeouts align with
42
  /// the server-side limit.
43
  ///
44
  /// See: https://supabase.com/docs/guides/functions/limits
45
  public static let requestIdleTimeout: TimeInterval = 150
46

47
  /// The base URL used to build per-function request URLs.
48
  ///
49
  /// Individual function URLs are formed by appending the function name to this URL,
50
  /// e.g. `https://<project-ref>.supabase.co/functions/v1/my-function`.
51
  public let url: URL
52

53
  /// The default region in which functions are invoked.
54
  ///
55
  /// Per-invocation overrides via ``FunctionInvokeOptions/region`` take
56
  /// precedence over this value. Pass `nil` to let Supabase route to the nearest region
57
  /// automatically.
58
  public let region: FunctionRegion?
59

60
  /// The JSON decoder used to decode response bodies in ``invokeDecodable(_:as:decoder:options:)``.
61
  ///
62
  /// Per-call override is also available via the `decoder`
63
  /// parameter of ``invokeDecodable(_:as:decoder:options:)``.
64
  public let decoder: JSONDecoder
65

66
  /// The HTTP headers sent with every request.
67
  ///
68
  /// Per-invocation headers supplied via ``FunctionInvokeOptions/headers`` are merged on
69
  /// top of these values, with the per-invocation values winning on collision.
70
  public private(set) var headers: [String: String] = [:]
36✔
71

72
  private let http: _HTTPClient
73

74
  /// Creates a `FunctionsClient` for standalone use (without a ``SupabaseClient``).
75
  ///
76
  /// Use this initialiser when you want to call Edge Functions independently, without the
77
  /// broader Supabase client stack. For most apps you should create a ``SupabaseClient`` and
78
  /// access its `functions` property instead.
79
  ///
80
  /// - Parameters:
81
  ///   - url: The base URL for the functions endpoint,
82
  ///     e.g. `https://<project-ref>.supabase.co/functions/v1`.
83
  ///   - headers: Additional headers included in every request. Defaults to an empty dictionary.
84
  ///     An `X-Client-Info` header is always added automatically.
85
  ///   - region: The default region to invoke functions in. Defaults to `nil` (automatic routing).
86
  ///   - session: The `URLSession` used to perform HTTP requests. Defaults to a new session with
87
  ///     ``requestIdleTimeout`` applied to `timeoutIntervalForRequest`.
88
  ///   - decoder: The `JSONDecoder` used by ``invokeDecodable(_:as:decoder:options:)``.
89
  ///     Defaults to `JSONDecoder()`.
90
  ///
91
  /// ## Example
92
  ///
93
  /// ```swift
94
  /// let functions = FunctionsClient(
95
  ///   url: URL(string: "https://<project-ref>.supabase.co/functions/v1")!,
96
  ///   headers: ["apikey": "<publishable-or-secret-key>", "Authorization": "Bearer <authorization-token>"]
97
  /// )
98
  /// ```
99
  public init(
100
    url: URL,
101
    headers: [String: String] = [:],
102
    region: FunctionRegion? = nil,
103
    session: URLSession = URLSession(configuration: .default),
104
    decoder: JSONDecoder = JSONDecoder()
105
  ) {
35✔
106
    self.init(
35✔
107
      url: url,
35✔
108
      headers: headers,
35✔
109
      region: region,
35✔
110
      session: session,
35✔
111
      decoder: decoder,
35✔
112
      tokenProvider: nil
35✔
113
    )
35✔
114
  }
35✔
115

116
  package init(
117
    url: URL,
118
    headers: [String: String] = [:],
119
    region: FunctionRegion? = nil,
120
    session: URLSession = URLSession(configuration: .default),
121
    decoder: JSONDecoder = JSONDecoder(),
122
    tokenProvider: TokenProvider?
123
  ) {
36✔
124
    self.url = url
36✔
125
    self.region = region
36✔
126
    self.decoder = decoder
36✔
127
    session.configuration.timeoutIntervalForRequest = Self.requestIdleTimeout
36✔
128
    self.http = _HTTPClient(
36✔
129
      host: url,
36✔
130
      session: session,
36✔
131
      tokenProvider: tokenProvider
36✔
132
    )
36✔
133
    self.headers = headers
36✔
134
    if self.headers["X-Client-Info"] == nil {
36✔
135
      self.headers["X-Client-Info"] = "functions-swift/\(version)"
35✔
136
    }
35✔
137
  }
36✔
138

139
  /// Updates the `Authorization` header used for subsequent requests.
140
  ///
141
  /// Pass a JWT to attach a `Bearer` token, or `nil` to remove the header entirely (e.g. for
142
  /// public functions that don't require authentication).
143
  ///
144
  /// When using ``SupabaseClient``, this method is called automatically whenever the
145
  /// authenticated session changes — you do not need to call it yourself.
146
  ///
147
  /// - Parameter token: A JWT access token, or `nil` to clear the authorization header.
148
  ///
149
  /// ## Example
150
  ///
151
  /// ```swift
152
  /// // Attach a token before invoking a protected function
153
  /// await functions.setAuth(token: session.accessToken)
154
  /// let (data, _) = try await functions.invoke("protected-function")
155
  ///
156
  /// // Remove the token for a public function call
157
  /// await functions.setAuth(token: nil)
158
  /// ```
159
  public func setAuth(token: String?) {
3✔
160
    if let token {
3✔
161
      headers["Authorization"] = "Bearer \(token)"
2✔
162
    } else {
2✔
163
      headers.removeValue(forKey: "Authorization")
1✔
164
    }
1✔
165
  }
3✔
166

167
  /// Invokes a function and decodes the JSON response body into the inferred `Decodable` type.
168
  ///
169
  /// The response body is decoded using the `decoder` parameter if provided, otherwise the
170
  /// instance-level ``decoder`` is used.
171
  ///
172
  /// - Parameters:
173
  ///   - functionName: The name of the Edge Function to invoke.
174
  ///   - decoder: An optional `JSONDecoder` to use for this call. When `nil`, falls back to the
175
  ///     instance ``decoder``. Defaults to `nil`.
176
  ///   - options: A closure that configures ``FunctionInvokeOptions`` before the request is sent.
177
  ///     Defaults to a no-op closure.
178
  /// - Returns: A tuple of the decoded value and the raw `HTTPURLResponse`.
179
  /// - Throws: ``FunctionsError`` on relay or HTTP errors, or a decoding error if the response
180
  ///   body cannot be decoded into `T`.
181
  ///
182
  /// ## Example
183
  ///
184
  /// ```swift
185
  /// struct HelloResponse: Decodable {
186
  ///   let message: String
187
  /// }
188
  ///
189
  /// let (response, _) = try await functions.invokeDecodable("hello", as: HelloResponse.self) {
190
  ///   $0.method = .get
191
  ///   $0.query = ["name": "world"]
192
  /// }
193
  /// print(response.message) // "Hello, world!"
194
  /// ```
195
  public func invokeDecodable<T: Decodable>(
196
    _ functionName: String,
197
    as _: T.Type = T.self,
198
    decoder: JSONDecoder? = nil,
199
    options applyOptions: (inout FunctionInvokeOptions) -> Void = { _ in }
4✔
200
  ) async throws -> (T, HTTPURLResponse) {
20✔
201
    let (data, response) = try await invoke(functionName, options: applyOptions)
20✔
202
    return (
20✔
203
      try (decoder ?? self.decoder).decode(T.self, from: data),
20✔
204
      response
20✔
205
    )
20✔
206
  }
20✔
207

208
  /// Invokes a function and returns the raw response body and `HTTPURLResponse`.
209
  ///
210
  /// Use this method when you need full control over response handling — for example, when the
211
  /// function returns non-JSON data, or when you want to inspect status codes and headers directly.
212
  ///
213
  /// - Parameters:
214
  ///   - functionName: The name of the Edge Function to invoke.
215
  ///   - options: A closure that configures ``FunctionInvokeOptions`` before the request is sent.
216
  ///     Defaults to a no-op closure.
217
  /// - Returns: A tuple of the raw `Data` body and the `HTTPURLResponse`.
218
  /// - Throws: ``FunctionsError/relayError`` if the relay reports an error,
219
  ///   ``FunctionsError/httpError(code:data:)`` for non-2xx responses, or a transport-level error.
220
  ///
221
  /// ## Example
222
  ///
223
  /// ```swift
224
  /// struct RequestBody: Encodable {
225
  ///   let userId: String
226
  /// }
227
  ///
228
  /// let (data, response) = try await functions.invoke("process-user") {
229
  ///   $0.method = .post
230
  ///   $0.body = try! JSONEncoder().encode(RequestBody(userId: "abc123"))
231
  ///   $0.headers["Content-Type"] = "application/json"
232
  /// }
233
  /// print(response.statusCode) // 200
234
  /// ```
235
  @discardableResult
236
  public func invoke(
237
    _ functionName: String,
238
    options applyOptions: (inout FunctionInvokeOptions) -> Void = { _ in }
5✔
239
  ) async throws -> (Data, HTTPURLResponse) {
29✔
240
    var options = FunctionInvokeOptions()
29✔
241
    applyOptions(&options)
29✔
242
    let (functionURL, method, query, allHeaders, body) = requestComponents(
29✔
243
      functionName: functionName,
29✔
244
      options: options
29✔
245
    )
29✔
246

29✔
247
    do {
29✔
248
      let (data, response) = try await http.fetchData(
29✔
249
        method,
29✔
250
        url: functionURL,
29✔
251
        query: query.isEmpty ? nil : query,
29✔
252
        body: body,
29✔
253
        headers: allHeaders.isEmpty ? nil : allHeaders
29✔
254
      )
29✔
255

27✔
256
      if response.value(forHTTPHeaderField: "x-relay-error") == "true" {
27✔
257
        throw FunctionsError.relayError
1✔
258
      }
26✔
259

26✔
260
      return (data, response)
26✔
261
    } catch let error as HTTPClientError {
29✔
262
      if case .responseError(let response, let data) = error {
1✔
263
        throw FunctionsError.httpError(code: response.statusCode, data: data)
1✔
264
      }
1✔
NEW
265
      throw error
×
266
    }
1✔
267
  }
29✔
268

269
  #if canImport(Darwin)
270
    /// Invokes a function and returns an async byte stream for the response body.
271
    ///
272
    /// Use this method for functions that return large payloads or use server-sent events /
273
    /// chunked transfer encoding. The stream yields individual `UInt8` bytes as they arrive.
274
    ///
275
    /// - Parameters:
276
    ///   - functionName: The name of the Edge Function to invoke.
277
    ///   - options: A closure that configures ``FunctionInvokeOptions`` before the request is sent.
278
    ///     Defaults to a no-op closure.
279
    /// - Returns: A tuple of an `AsyncThrowingStream<UInt8, Error>` and the initial
280
    ///   `HTTPURLResponse`.
281
    /// - Throws: ``FunctionsError/relayError`` if the relay reports an error,
282
    ///   ``FunctionsError/httpError(code:data:)`` for non-2xx responses, or a transport-level error.
283
    ///
284
    /// ## Example
285
    ///
286
    /// ```swift
287
    /// let (stream, _) = try await functions.invokeStream("stream-data")
288
    ///
289
    /// var buffer = Data()
290
    /// for try await byte in stream {
291
    ///   buffer.append(byte)
292
    /// }
293
    /// print(String(data: buffer, encoding: .utf8) ?? "")
294
    /// ```
295
    @available(macOS 12.0, *)
296
    public func invokeStream(
297
      _ functionName: String,
298
      options applyOptions: (inout FunctionInvokeOptions) -> Void = { _ in }
3✔
299
    ) async throws -> (AsyncThrowingStream<UInt8, any Error>, HTTPURLResponse) {
3✔
300
      var options = FunctionInvokeOptions()
3✔
301
      applyOptions(&options)
3✔
302
      let (functionURL, method, query, allHeaders, body) = requestComponents(
3✔
303
        functionName: functionName,
3✔
304
        options: options
3✔
305
      )
3✔
306

3✔
307
      do {
3✔
308
        let (bytes, response) = try await http.fetchStream(
3✔
309
          method,
3✔
310
          url: functionURL,
3✔
311
          query: query.isEmpty ? nil : query,
3✔
312
          body: body,
3✔
313
          headers: allHeaders.isEmpty ? nil : allHeaders
3✔
314
        )
3✔
315

2✔
316
        if response.value(forHTTPHeaderField: "x-relay-error") == "true" {
2✔
317
          throw FunctionsError.relayError
1✔
318
        }
1✔
319

1✔
320
        return (bytes, response)
1✔
321
      } catch let error as HTTPClientError {
3✔
322
        if case .responseError(let response, let data) = error {
1✔
323
          throw FunctionsError.httpError(code: response.statusCode, data: data)
1✔
324
        }
1✔
NEW
325
        throw error
×
326
      }
1✔
327
    }
3✔
328
  #endif
329

330
  private func requestComponents(
331
    functionName: String,
332
    options: FunctionInvokeOptions
333
  ) -> (
334
    url: URL,
335
    method: HTTPMethod,
336
    query: [String: String],
337
    headers: [String: String],
338
    body: RequestBody?
339
  ) {
32✔
340
    let method =
32✔
341
      options.method.flatMap { HTTPMethod(rawValue: $0.rawValue) } ?? .post
32✔
342
    var query = options.query
32✔
343
    var allHeaders = headers.merging(options.headers) { _, new in new }
32✔
344

32✔
345
    if let region = (options.region ?? region)?.rawValue {
32✔
346
      allHeaders["x-region"] = region
2✔
347
      query["forceFunctionRegion"] = region
2✔
348
    }
2✔
349

32✔
350
    let body: RequestBody? = options.body.map { .data($0) }
32✔
351
    return (
32✔
352
      url.appendingPathComponent(functionName), method, query, allHeaders, body
32✔
353
    )
32✔
354
  }
32✔
355
}
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