diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md new file mode 100644 index 00000000..60bbf1af --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md @@ -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.