-
Notifications
You must be signed in to change notification settings - Fork 149
Description
Motivation
Hi! I am the developer of the swift-http-error-handling package, which helps with interpreting HTTP responses and handling failures. I am looking to apply some of the concepts from that package to my usage of swift-openapi-generator, but I am running into some issues.
Overview of swift-http-error-handling
A core part of swift-http-error-handling is interpreting an HTTP response as a success or a failure.
Representing an HTTP Application Failure
The package introduces an HTTPApplicationError type to represent a failure. This allows the failure to be propagated and handled using Swift’s standard try-catch error handling mechanism. The error type contains the following information:
- The
HTTPResponsevalue. - A generically-typed value representing the response body, if desired. Some HTTP servers add additional details about a failure to the response body.
- Whether the failure is transient. This is important for the package’s retry feature.
Interpreting HTTP Responses
The package also extends HTTPResponse to add two methods: throwIfFailed(successStatuses:transientFailureStatuses:) and throwIfFailed(successStatuses:transientFailureStatuses:makeResponseBody:).
The methods have default values for the successStatuses and transientFailureStatuses parameters so that the methods can be called conveniently:
try response.throwIfFailed()The successStatuses and transientFailureStatuses parameters allow for a customized interpretation if needed:
try response.throwIfFailed(
successStatuses: [.created],
transientFailureStatuses: HTTPResponse.Status.transientFailures.union([.conflict])
)The second throwIfFailed method accepts an async closure to attach a response body to the error:
try await response.throwIfFailed {
return try await deserializeFailureDetails(from: responseBody)
}The response body can be accessed later like so:
do {
try await performRequest()
} catch let error as HTTPApplicationError<MyFailureDetails> {
let failureDetails = error.responseBody
doSomething(with: failureDetails)
}Shortcomings of the OpenAPI-Generated Client
If I wanted to interpret an OpenAPI-generated response as a success or failure, I might do the following:
let response = try await client.getGreeting()
doSomething(with: try response.ok.body.json)If the HTTP response status was not .ok, then the response.ok property access would throw RuntimeError.unexpectedResponseStatus. This seems like it works for our use case; however, there are a number of issues with this approach.
Unsafe
A developer needs to remember to check the response status. Not checking the response status introduces a subtle bug.
This is obvious for experienced developers who have used Swift HTTP libraries like Foundation or AsyncHTTPClient but may be non-intuitive for other developers. Even experienced developers may forget to check the response status in cases where they do not intend to access the response body.
Inconvenient
Consider the case where a response can have multiple successful statuses (e.g. .ok, .notFound, and .gone for a delete request). The developer would have to do one of two things, both of which are inconvenient:
let response = try await client.deleteSomething()
do {
_ = try response.ok
} catch {
do {
_ = try response.notFound
} catch {
_ = try response.gone
}
}let response = try await client.deleteSomething()
switch response {
case .ok, .notFound, .gone:
break
default:
throw MyCustomError() // need to define our own error type
Limited Error Handling Ability
The ability to handle such an error is limited by the following:
- The error type is not public.
- The error does not contain the response body.
Duplicate Logic in Middleware
Some middleware also needs to interpret the response as a success or failure (e.g. to retry, to log failures, etc.). Now, the response interpretation logic is duplicated across the caller and the middleware, which is both inconvenient and could cause inconsistency across the two implementations, causing bugs.
Proposed Solution
I propose a breaking change for swift-openapi-generator version 2.0.0 to address all of the current shortcomings.
I propose that the client automatically interprets responses and throws HTTPApplicationError when the response represents a failure.
Since the response deserialization code is inside of the client, the client could deserialize the response body, if any, and attach it to the error automatically. Error handling code would be able to catch the error using its concrete type HTTPApplicationError<Operations.myOperation.Output> or as an instance of HTTPApplicationErrorProtocol if the code does not care about the response body.
The generated client could expose properties to customize the response interpretation. The generated client methods could also have additional parameters to customize the response interpretation per operation.
Finally, it would be important for the client to interpret the response before passing the response to the middleware so that the middleware do not have to do their own response interpretation.
Putting it all together, it would look something like the following:
let client = Client(serverURL: URL(string: "http://localhost:8080/api")!,
transport: URLSessionTransport(),
// The middleware will handle HTTP application failures using the standard
// try-catch mechanism.
middlewares: [RetryMiddleware(), LoggingMiddleware()])
do {
// Safer than before.
_ = try await client.doSomething()
// More convenient than before.
_ = try await client.deleteSomething(successStatuses: [.ok, .notFound, .gone])
} catch let error as any HTTPApplicationErrorProtocol {
// The error type is public, allowing for more sophisticated error handling.
doSomething(with: error)
}Alternatives Considered
Alternative 1: Add middleware that interprets the response
Instead of building the response interpretation into the client directly, swift-openapi-runtime or a third-party library could expose a hypothetical ResponseInterpretationMiddleware that does the response interpretation. Developers would add ResponseInterpretationMiddleware to the list of middleware when creating the client.
The first issue with this alternative is that middleware do not have access to the response deserialization code, so ResponseInterpretationMiddleware would not be able to attach the deserialized response body to the error.
Second, since the ResponseInterpretationMiddleware is optional, other middleware would not be able to rely on its availability and would need to do their own response interpretation, duplicating logic.
Finally, this alternative is less safe compared to the proposed solution since the developer would need to remember to add the ResponseInterpretationMiddleware to the list of middleware.
Alternative 2: Add a property to the generated response that returns the HTTPResponse value
In this alternative, the generated response would have a property to access the HTTPResponse value. The caller could then interpret the response themselves.
This alternative does not solve the safety issue since the developer still needs to remember to interpret the response.
This alternative also does not solve the middleware code duplication issue since the middleware would still need to do their own response interpretation.
Additional Information
If the proposed solution is accepted, feel free to add a dependency to my swift-http-error-handling package to help with implementation. You would gain access to the HTTPApplicationError type, the throwIfFailed methods, the default set of success statuses, and the default set of transient failure statuses.
I promise I won’t do anything sketchy with my package. 😆 But if it makes you feel better, I am also considering creating a proposal to merge my package into the swift-http-types package. Let me know if that would be a prerequisite. If it is, I’ll get right on it!