Skip to content

Commit 3ecfcfa

Browse files
gayathrisairamGayathri Sairamkrishnanczechboy0
authored
[Proposal] SOAR-0011: Improved error handling (#626)
### Motivation Swift OpenAPI runtime doesn't allow for fine grained error handling. This PR adds a proposal for improved error handling in Swift OpenAPI runtime. ### Modifications - Add SOAR-011: Improved error handling. (See the [proposal](https://github.com/gayathrisairam/swift-openapi-generator/blob/error_handling/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md) for details) ### Result n/a ### Test Plan n/a --------- Co-authored-by: Gayathri Sairamkrishnan <[email protected]> Co-authored-by: Honza Dvorsky <[email protected]>
1 parent 31fa50a commit 3ecfcfa

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or
5252
- <doc:SOAR-0008>
5353
- <doc:SOAR-0009>
5454
- <doc:SOAR-0010>
55+
- <doc:SOAR-0011>
5556
- <doc:SOAR-0012>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# SOAR-0011: Improved Error Handling
2+
3+
Improve error handling by adding the ability for mapping application errors to HTTP responses.
4+
5+
## Overview
6+
7+
- Proposal: SOAR-0011
8+
- Author(s): [Gayathri Sairamkrishnan](https://github.com/gayathrisairam)
9+
- Status: **Implemented**
10+
- Issue: [apple/swift-openapi-generator#609](https://github.com/apple/swift-openapi-generator/issues/609)
11+
- Affected components:
12+
- runtime
13+
- Versions:
14+
- v1.0 (2024-09-19): Initial version
15+
- v1.1(2024-10-07):
16+
- Replace the proposed solution to have a single error handling protocol, with the status being required and
17+
headers/body being optional.
18+
19+
### Introduction
20+
21+
The goal of this proposal to improve the current error handling mechanism in Swift OpenAPI runtime. The proposal
22+
introduces a way for users to map errors thrown by their handlers to specific HTTP responses.
23+
24+
### Motivation
25+
26+
When implementing a server with Swift OpenAPI Generator, users implement a type that conforms to a generated protocol,
27+
providing one method for each API operation defined in the OpenAPI document. At runtime, if this function throws, it's up to the server transport to transform it into an HTTP response status code – for example, some transport use `500 Internal Error`.
28+
29+
Instead, server developers may want to map errors thrown by the application to a more specific HTTP response.
30+
Currently, this can be achieved by checking for each error type in each handler's catch block, converting it to an
31+
appropriate HTTP response and returning it.
32+
33+
For example,
34+
```swift
35+
func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output {
36+
do {
37+
let response = try callGreetingLib()
38+
return .ok(.init(body: response))
39+
} catch let error {
40+
switch error {
41+
case GreetingError.authorizationError:
42+
return .unauthorized(.init())
43+
case GreetingError.timeout:
44+
return ...
45+
}
46+
}
47+
}
48+
```
49+
If a user wishes to map many errors, the error handling block scales linearly and introduces a lot of ceremony.
50+
51+
### Proposed solution
52+
53+
The proposed solution is twofold.
54+
55+
1. Provide a protocol in `OpenAPIRuntime` to allow users to extend their error types with mappings to HTTP responses.
56+
57+
2. Provide an (opt-in) middleware in OpenAPIRuntime that will call the conversion function on conforming error types when
58+
constructing the HTTP response.
59+
60+
Vapor has a similar mechanism called [AbortError](https://docs.vapor.codes/basics/errors/).
61+
62+
Hummingbird also has an [error handling mechanism](https://docs.hummingbird.codes/2.0/documentation/hummingbird/errorhandling/)
63+
by allowing users to define a [HTTPError](https://docs.hummingbird.codes/2.0/documentation/hummingbird/httperror)
64+
65+
The proposal aims to provide a transport agnostic error handling mechanism for OpenAPI users.
66+
67+
### Detailed design
68+
69+
#### Proposed Error protocols
70+
71+
Users can choose to conform to the error handling protocol below and optionally provide the optional fields depending on
72+
the level of specificity they would like to have in the response.
73+
74+
```swift
75+
public protocol HTTPResponseConvertible {
76+
var httpStatus: HTTPResponse.Status { get }
77+
var httpHeaderFields: HTTPTypes.HTTPFields { get }
78+
var httpBody: OpenAPIRuntime.HTTPBody? { get }
79+
}
80+
81+
extension HTTPResponseConvertible {
82+
var httpHeaderFields: HTTPTypes.HTTPFields { [:] }
83+
var httpBody: OpenAPIRuntime.HTTPBody? { nil }
84+
}
85+
```
86+
87+
#### Proposed Error Middleware
88+
89+
The proposed error middleware in OpenAPIRuntime will convert the application error to the appropriate error response.
90+
It returns 500 for application error(s) that do not conform to HTTPResponseConvertible protocol.
91+
92+
```swift
93+
public struct ErrorHandlingMiddleware: ServerMiddleware {
94+
func intercept(_ request: HTTPTypes.HTTPRequest,
95+
body: OpenAPIRuntime.HTTPBody?,
96+
metadata: OpenAPIRuntime.ServerRequestMetadata,
97+
operationID: String,
98+
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
99+
do {
100+
return try await next(request, body, metadata)
101+
} catch let error as ServerError {
102+
if let appError = error.underlyingError as? HTTPResponseConvertible else {
103+
return (HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
104+
appError.httpBody)
105+
} else {
106+
throw error
107+
}
108+
}
109+
}
110+
}
111+
```
112+
113+
Please note that the proposal places the responsibility to conform to the documented API in the hands of the user.
114+
There's no mechanism to prevent the users from inadvertently transforming a thrown error into an undocumented response.
115+
116+
#### Example usage
117+
118+
1. Create an error type that conforms to the error protocol
119+
```swift
120+
extension MyAppError: HTTPResponseConvertible {
121+
var httpStatus: HTTPResponse.Status {
122+
switch self {
123+
case .invalidInputFormat:
124+
.badRequest
125+
case .authorizationError:
126+
.forbidden
127+
}
128+
}
129+
}
130+
```
131+
132+
2. Opt in to the error middleware while registering the handler
133+
134+
```swift
135+
let handler = try await RequestHandler()
136+
try handler.registerHandlers(on: transport, middlewares: [ErrorHandlingMiddleware()])
137+
138+
```
139+
140+
### API stability
141+
142+
This feature is purely additive:
143+
- Additional APIs in the runtime library
144+
145+
146+
### Future directions
147+
148+
A possible future direction is to add the error middleware by default by changing the [default value for the middlewares](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Interface/UniversalServer.swift#L56)
149+
argument in handler initialisation.
150+
151+
### Alternatives considered
152+
153+
An alternative here is to invoke the error conversion function directly from OpenAPIRuntime's handler. The feature would
154+
still be opt-in as users have to explicitly conform to the new error protocols.
155+
156+
However, there is a rare case where an application might depend on a library (for eg: an auth library) which in turn
157+
depends on OpenAPIRuntime. If the authentication library conforms to the new error protocols, this would result in a
158+
breaking change for the application, whereas an error middleware provides flexibility to the user on whether they
159+
want to subscribe to the new behaviour or not.

0 commit comments

Comments
 (0)