-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Gayathri Sairamkrishnan
committed
Sep 17, 2024
1 parent
7c36ba9
commit 7fe0b73
Showing
1 changed file
with
151 additions
and
0 deletions.
There are no files selected for viewing
151 changes: 151 additions & 0 deletions
151
Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# SOAR-0011: Improved Error Handling | ||
|
||
Improve error handling by adding the ability for mapping application errors to HTTP responses. | ||
|
||
## Overview | ||
|
||
- Proposal: SOAR-0011 | ||
- Author(s): [Gayathri Sairamkrishnan](https://github.com/gayathrisairam) | ||
- Status: **Awaiting Review** | ||
- Issue: [apple/swift-openapi-generator#609](https://github.com/apple/swift-openapi-generator/issues/609) | ||
- Affected components: | ||
- runtime | ||
|
||
### Introduction | ||
|
||
The goal of this proposal to improve the current error handling mechanism in Swift OpenAPI runtime. The proposal | ||
introduces a way for users to map errors thrown by their handlers to specific HTTP responses. | ||
|
||
### Motivation | ||
|
||
When implementing a server with Swift OpenAPI Generator, users implement a type that conforms to a generated protocol, providing one method for each API operation defined in the OpenAPI document. At runtime, if this function throws, the runtime library transforms this into a 500 HTTP response (Internal Error). | ||
|
||
Instead, server developers may want to map errors thrown by the application to a more specific HTTP response. | ||
Currently, this can be achieved by checking for each error type in each handler's catch block, converting it to an | ||
appropriate HTTP response and returning it. | ||
|
||
For example, | ||
```swift | ||
func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output { | ||
do { | ||
let response = try callGreetingLib() | ||
return .ok(.init(body: response)) | ||
} catch let error { | ||
switch error { | ||
case GreetingError.authorizationError: | ||
return (HTTPResponse(status: 404), nil) | ||
case GreetingError.timeout: | ||
return ... | ||
} | ||
} | ||
} | ||
``` | ||
If a user wishes to map many errors, the error handling block scales linearly and introduces a lot of ceremony. | ||
|
||
### Proposed solution | ||
|
||
The proposed solution is twofold. | ||
|
||
1. Provide protocol(s) in `OpenAPIRuntime` to allow users to extend their error types with mappings to HTTP responses. | ||
|
||
2. Provide an (opt-in) middleware in OpenAPIRuntime that will call the conversion function on conforming error types when | ||
constructing the HTTP response. | ||
|
||
Vapor has a similar mechanism called [AbortError](https://docs.vapor.codes/basics/errors/). | ||
|
||
HummingBird also has an [error handling mechanism](https://docs.hummingbird.codes/2.0/documentation/hummingbird/errorhandling/) | ||
by allowing users to define a [HTTPError](https://docs.hummingbird.codes/2.0/documentation/hummingbird/httperror) | ||
|
||
The proposal aims to provide a transport agnostic error handling mechanism for OpenAPI users. | ||
|
||
### Detailed design | ||
|
||
#### Proposed Error protocols | ||
|
||
Users can choose to conform to one of the protocols described below depending on the level of specificity they would | ||
like to have in the response. | ||
|
||
```swift | ||
public protocol HTTPStatusConvertible { | ||
var httpStatus: HTTPResponse.Status { get } | ||
} | ||
|
||
public protocol HTTPHeadConvertible: HTTPStatusConvertible { | ||
var httpHeaderFields: HTTPTypes.HTTPFields { get } | ||
} | ||
|
||
public protocol HTTPResponseConvertible: HTTPHeadConvertible { | ||
var httpBody: OpenAPIRuntime.HTTPBody { get } | ||
} | ||
``` | ||
|
||
#### Proposed Error Middleware | ||
|
||
The proposed error middleware in OpenAPIRuntime will convert the application error to the appropriate error response. | ||
```swift | ||
public struct ErrorMiddleware: ServerMiddleware { | ||
func intercept(_ request: HTTPTypes.HTTPRequest, | ||
body: OpenAPIRuntime.HTTPBody?, | ||
metadata: OpenAPIRuntime.ServerRequestMetadata, | ||
operationID: String, | ||
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { | ||
do { | ||
return try await next(request, body, metadata) | ||
} catch let error as ServerError { | ||
if let appError = error.underlyingError as? HTTPStatusConvertible else { | ||
return (HTTPResponse(status: appError.httpStatus), nil) | ||
} else if ... { | ||
|
||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Please note that the proposal places the responsibility to conform to the documented API in the hands of the user. | ||
There's no mechanism to prevent the users from inadvertently transforming a thrown error into an undocumented response. | ||
|
||
#### Example usage | ||
|
||
1. Create an error type that conforms to one of the error protocol(s) | ||
```swift | ||
extension MyAppError: HTTPStatusConvertible { | ||
var httpStatus: HTTPResponse.Status { | ||
switch self { | ||
case .invalidInputFormat: | ||
HTTPResponse.Status.badRequest | ||
case .authorizationError: | ||
HTTPResponse.Status.forbidden | ||
} | ||
} | ||
} | ||
``` | ||
|
||
2. Opt in to the error middleware while registering the handler | ||
|
||
```swift | ||
let handler = try await RequestHandler() | ||
try handler.registerHandlers(on: transport, middlewares: [ErrorHandlingMiddleware()]) | ||
|
||
``` | ||
|
||
### API stability | ||
|
||
This feature is purely additive: | ||
- Additional APIs in the runtime library | ||
|
||
|
||
### Future directions | ||
|
||
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) | ||
argument in handler initialisation. | ||
|
||
### Alternatives considered | ||
|
||
An alternative here is to invoke the error conversion function directly from OpenAPIRuntime's handler. The feature would | ||
still be opt-in as users have to explicitly conform to the new error protocols. | ||
|
||
However, there is a rare case where an application might depend on a library (for eg: an auth library) which in turn | ||
depends on OpenAPIRuntime. If the authentication library conforms to the new error protocols, this would result in a | ||
breaking change for the application, whereas an error middleware provides flexibility to the user on whether they | ||
want to subscribe to the new behaviour or not. |