Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] SOAR-0011: Improved error handling #626

Merged
merged 14 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or
- <doc:SOAR-0008>
- <doc:SOAR-0009>
- <doc:SOAR-0010>
- <doc:SOAR-0011>
- <doc:SOAR-0012>
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# 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: **Ready for Implementation**
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
- Issue: [apple/swift-openapi-generator#609](https://github.com/apple/swift-openapi-generator/issues/609)
- Affected components:
- runtime
- Versions:
- v1.0 (2024-09-19): Initial version
- v1.1(2024-10-07):
- Replace the proposed solution to have a single error handling protocol, with the status being required and
headers/body being optional.

### 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, it's up to the server transport to transform it into an HTTP response status code – for example, some transport use `500 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 .unauthorized(.init())
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 a protocol 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 the error handling protocol below and optionally provide the optional fields depending on
the level of specificity they would like to have in the response.

```swift
public protocol HTTPResponseConvertible {
var httpStatus: HTTPResponse.Status { get }
var httpHeaderFields: HTTPTypes.HTTPFields { get }
var httpBody: OpenAPIRuntime.HTTPBody? { get }
}

extension HTTPResponseConvertible {
var httpHeaderFields: HTTPTypes.HTTPFields { [:] }
var httpBody: OpenAPIRuntime.HTTPBody? { nil }
}
```

#### Proposed Error Middleware

The proposed error middleware in OpenAPIRuntime will convert the application error to the appropriate error response.
It returns 500 for application error(s) that do not conform to HTTPResponseConvertible protocol.

```swift
public struct ErrorHandlingMiddleware: 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? HTTPResponseConvertible else {
return (HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
appError.httpBody)
} else {
throw error
}
}
}
}
```

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 the error protocol
```swift
extension MyAppError: HTTPResponseConvertible {
var httpStatus: HTTPResponse.Status {
switch self {
case .invalidInputFormat:
.badRequest
case .authorizationError:
.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()])
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved

```

### 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.