Repository navigation
Expand file tree
/
Copy pathFunctionsAPI.swift
More file actions
125 lines (115 loc) · 4.75 KB
/
Copy pathFunctionsAPI.swift
File metadata and controls
125 lines (115 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//
// FunctionsAPI.swift
// Functions
//
// Created by Guilherme Souza on 05/10/26.
//
import Foundation
import HTTPTypes
import Helpers
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// The one place a request is built, sent and its head classified.
///
/// Both `invoke` and `stream` go through ``exchange(name:body:options:configuration:http:)``;
/// nothing else inspects a response head or relabels a transport failure.
enum FunctionsAPI {
/// Sent unless the configured or per-call headers set the same name.
static let defaultHeaders: HTTPFields = [.xClientInfo: clientInfo(library: "functions-swift")]
/// Builds the request head and body for one invocation. Pure: no token lookup, no I/O.
///
/// Headers merge lowest to highest: `X-Client-Info`, then `configuration.headers`, then the
/// body's `Content-Type`, then `options.headers`. The body's type sits above the client
/// headers so a client-wide `Content-Type` cannot relabel a JSON body; a per-call one still
/// wins. A region, per call or client-wide, goes out as both the `x-region` header and the
/// `forceFunctionRegion` query item, which is what the gateway reads.
static func makeRequest(
name: String,
body: FunctionBody?,
options: FunctionInvokeOptions,
configuration: FunctionsClient.Configuration
) -> (HTTPRequest, HTTPBody?) {
var headers = defaultHeaders.merging(with: configuration.headers)
if let body { headers[.contentType] = body.contentType }
headers.merge(with: options.headers)
var query = options.query
if let region = options.region ?? configuration.region {
headers[.xRegion] = region.rawValue
query.appendOrUpdate(URLQueryItem(name: "forceFunctionRegion", value: region.rawValue))
}
let request = HTTPRequest(
method: options.method,
url: configuration.url.appendingPathComponent(name),
query: query,
headerFields: headers
)
return (request, body?.httpBody)
}
/// Builds and sends one invocation, returning the head and the unread body once the head
/// passes ``throwIfFailed(_:body:)``. The bearer token is `AccessTokenMiddleware`'s job.
static func exchange(
name: String,
body: FunctionBody?,
options: FunctionInvokeOptions,
configuration: FunctionsClient.Configuration,
http: HTTPClient
) async throws -> (HTTPResponse, HTTPBody?) {
let (request, requestBody) = makeRequest(
name: name, body: body, options: options, configuration: configuration)
do {
let (head, responseBody) = try await http.stream(
request, body: requestBody, timeout: options.timeout)
try await throwIfFailed(head, body: responseBody)
return (head, responseBody)
} catch {
throw mapTransportError(error)
}
}
/// Throws ``FunctionsError`` with kind `.relay` or `.server` when `head` is a failure, carrying
/// at most `HTTPClient.maxErrorBodyBytes` of `body` and the platform's `sb-error-code`.
///
/// The relay check runs first: relay failures are non-2xx too.
static func throwIfFailed(_ head: HTTPResponse, body: HTTPBody?) async throws {
let kind: FunctionsError.Kind
let message: String
if head.headerFields[.xRelayError] == "true" {
kind = .relay
message = "Relay Error invoking the Edge Function"
} else if head.status.kind != .successful {
kind = .server
message = "Edge Function returned a non-2xx status code: \(head.status.code)"
} else {
return
}
// A relay failure can arrive with a 2xx status, which `HTTPClient` does not cap.
let data =
if let body {
try await Data(collecting: body, truncatingAt: HTTPClient.maxErrorBodyBytes)
} else {
Data()
}
throw FunctionsError(
kind: kind,
message: message,
code: head.headerFields[.sbErrorCode].map(FunctionsError.Code.init(rawValue:)),
response: HTTPErrorResponse(head, body: data)
)
}
/// Relabels the network layer's own failures as `.transport` and a token provider's failure as
/// `.accessToken`. `CancellationError`, a `FunctionsError` and anything else thrown by user code
/// that runs inside the exchange (a custom `ClientTransport` or middleware) propagate as
/// themselves, unless it is a `URLError`: the chain cannot tell one of those from the
/// transport's own.
static func mapTransportError(_ error: any Error) -> any Error {
if let failure = error as? AccessTokenFailure {
return FunctionsError(
kind: .accessToken,
message: AccessTokenFailure.message,
underlyingError: failure.underlyingError)
}
guard let urlError = error as? URLError else { return error }
return FunctionsError(
kind: .transport, message: urlError.localizedDescription, underlyingError: urlError)
}
}