Skip to content

Commit cbd777f

Browse files
committed
[Docs] Error handling
1 parent 1db77dd commit cbd777f

File tree

5 files changed

+162
-4
lines changed

5 files changed

+162
-4
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ let package = Package(
5454
// Tests-only: Runtime library linked by generated code, and also
5555
// helps keep the runtime library new enough to work with the generated
5656
// code.
57-
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.3.2"),
57+
// .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.3.2"),
58+
.package(path: "../swift-openapi-runtime"),
5859
.package(url: "https://github.com/apple/swift-http-types", from: "1.0.2"),
5960
],
6061
targets: [
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Handling errors on clients and servers
2+
3+
Learn about the default error-handling behavior and how to customize it.
4+
5+
## Overview
6+
7+
Generated clients and servers have a default error-handling behavior, which you can change using several customization points.
8+
9+
### Understand the default error-handling behavior
10+
11+
Generated **`Client`** structs throw an error from any of the generated operation methods when:
12+
- the request fails to serialize
13+
- any middleware throws an error
14+
- the transport throws an error
15+
- the response fails to deserialize
16+
17+
The thrown error type is always an instance of [`ClientError`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienterror), which holds additional context about the request, useful for debugging. The error also contains the properties [`causeDescription`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienterror/causedescription), providing a human-readable high level category of the error, and [`underlyingError`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienterror/underlyingError), the original error. If an error is thrown in a middleware or the transport, it gets provided in the `underlyingError` property.
18+
19+
> Tip: The extra context provided by `ClientError` helps with debugging a failed request, especially when the error is caught higher up the stack after multiple calls to `Client` occurred in the same scope.
20+
21+
Similarly on the server, the **`registerHandlers`** method throws an error up to the middleware/transport chain when:
22+
- the request fails to deserialize
23+
- any middleware throws an error
24+
- the user handler throws an error
25+
- the response fails to serialize
26+
27+
The thrown error type is always an instance of [`ServerError`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servererror), which holds additional context about the request, useful for debugging. The error also contains the properties [`causeDescription`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servererror/causedescription), providing a human-readable high level category of the error, and [`underlyingError`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servererror/underlyingError), the original error. If an error is thrown in a middleware or the handler, it gets provided in the `underlyingError` property.
28+
29+
### Customize the thrown error using an error mapper
30+
31+
In situations when your existing code inspects the thrown error beyond just logging it, you might need to customize the thrown error, or completely discard the `ClientError`/`ServerError` context, and only propagate the original `underlyingError`.
32+
33+
To customize the client error-throwing behavior, provide the `clientErrorMapper` closure when instantiating your `Configuration`:
34+
35+
```swift
36+
let client = Client(
37+
serverURL: try Servers.server1.url(),
38+
configuration: .init(clientErrorMapper: { clientError in
39+
// Always throw the underlying error, discard the extra context
40+
clientError.underlyingError
41+
}),
42+
transport: transport
43+
)
44+
45+
do {
46+
let response = try await client.greet() // throws an error
47+
} catch {
48+
print(error) // this error is now the underlyingError, rather than ClientError
49+
}
50+
```
51+
52+
On the server, provide the customized `Configuration` to the `registerHandlers` call:
53+
54+
```swift
55+
try myHandler.registerHandlers(
56+
on: transport,
57+
configuration: .init(
58+
serverErrorMapper: { serverError in
59+
// Always throw the underlying error, discard the extra context
60+
serverError.underlyingError
61+
}
62+
)
63+
)
64+
```
65+
66+
This error customization point can also be used for collecting telemetry about the types of errors thrown, by emitting the metric and returning the unmodified error from the closure.
67+
68+
### Convert errors into specific HTTP response status codes
69+
70+
When implementing a server, it can be useful to reuse the same utility code from multiple type-safe handler methods, and map certain errors to specific HTTP response status codes.
71+
72+
> Warning: Use this customization point with care by ensuring that you only map errors to the HTTP response status codes allowed by your OpenAPI document.
73+
74+
Consider an example where your server calls an upstream service that has limited capacity, and some of those calls fail when the service is overloaded. Your service would want to return the HTTP status code 429 to instruct the client to retry later.
75+
76+
Define an error type that represents such an error:
77+
78+
```swift
79+
struct UpstreamServiceOverloaded: Error {}
80+
```
81+
82+
And conform it to the [`HTTPResponseConvertible`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/httpresponseconvertible) protocol, by returning the `.tooManyRequests` (429) status and a `retry-after` HTTP header field asking the client to try again in 15 seconds.
83+
84+
```swift
85+
extension UpstreamServiceOverloaded: HTTPResponseConvertible {
86+
var httpStatus: HTTPResponse.Status {
87+
.tooManyRequests
88+
}
89+
90+
var httpHeaderFields: HTTPTypes.HTTPFields {
91+
[.retryAfter: "15"]
92+
}
93+
}
94+
```
95+
96+
Finally, for this error to get converted into an HTTP response whenever it's thrown in any user handler or middleware, add the [`ErrorHandlingMiddleware`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/errorhandlingmiddleware) to the middlewares array when calling `registerHandlers`:
97+
98+
```swift
99+
try myHandler.registerHandlers(
100+
on: transport,
101+
middlewares: [
102+
ErrorHandlingMiddleware()
103+
]
104+
)
105+
```
106+
107+
> Note: When the response (for example, the 429 from above) is documented in the OpenAPI document, it is still preferable to return it explicitly from your type-safe handler, making it easier to ensure you only return documented responses. However, this customization point exists for cases where propagating the error through the handler is impractical or overly repetitive.

Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,9 @@ components:
175175
- <doc:Useful-OpenAPI-patterns>
176176
- <doc:Supported-OpenAPI-features>
177177
178-
### Generator plugin and CLI
178+
### Customization
179179
- <doc:Configuring-the-generator>
180+
- <doc:Handling-errors-on-clients-and-servers>
180181
- <doc:Manually-invoking-the-generator-CLI>
181182
- <doc:Frequently-asked-questions>
182183

Tests/PetstoreConsumerTests/Test_Client.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,35 @@ final class Test_Client: XCTestCase {
527527
do {
528528
_ = try await client.getStats(.init())
529529
XCTFail("Should have thrown an error")
530-
} catch {}
530+
} catch {
531+
XCTAssertTrue(error is ClientError)
532+
}
533+
}
534+
535+
func testGetStats_200_unexpectedContentType_customErrorMapper() async throws {
536+
transport = .init { request, requestBody, baseURL, operationID in
537+
XCTAssertEqual(operationID, "getStats")
538+
XCTAssertEqual(request.path, "/pets/stats")
539+
XCTAssertEqual(request.method, .get)
540+
XCTAssertNil(requestBody)
541+
return try HTTPResponse(status: .ok, headerFields: [.contentType: "foo/bar"])
542+
.withEncodedBody(
543+
#"""
544+
count_is_1
545+
"""#
546+
)
547+
}
548+
let client = Client(
549+
serverURL: try URL(validatingOpenAPIServerURL: "/api"),
550+
configuration: .init(clientErrorMapper: { $0.underlyingError }),
551+
transport: transport
552+
)
553+
do {
554+
_ = try await client.getStats(.init())
555+
XCTFail("Should have thrown an error")
556+
} catch {
557+
XCTAssertFalse(error is ClientError)
558+
}
531559
}
532560

533561
func testPostStats_202_json() async throws {

Tests/PetstoreConsumerTests/Test_Server.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,28 @@ final class Test_Server: XCTestCase {
403403
.init()
404404
)
405405
XCTFail("Should have thrown an error.")
406-
} catch {}
406+
} catch {
407+
XCTAssertTrue(error is ServerError)
408+
}
409+
}
410+
411+
func testGetStats_200_unexpectedAccept_customErrorMapper() async throws {
412+
client = .init(getStatsBlock: { input in .ok(.init(body: .json(.init(count: 1)))) })
413+
let server = TestServerTransport()
414+
try client.registerHandlers(
415+
on: server,
416+
configuration: .init(serverErrorMapper: { $0.underlyingError })
417+
)
418+
do {
419+
_ = try await server.getStats(
420+
.init(soar_path: "/api/pets/stats", method: .patch, headerFields: [.accept: "foo/bar"]),
421+
nil,
422+
.init()
423+
)
424+
XCTFail("Should have thrown an error.")
425+
} catch {
426+
XCTAssertFalse(error is ServerError)
427+
}
407428
}
408429

409430
func testGetStats_200_text() async throws {

0 commit comments

Comments
 (0)