Open cantbetrue opened 2 months ago
Hi @cantbetrue,
throwing a ServerError
is part of the API contract, providing context that's important for debugging.
If you want to get access to the original underlying error, for example thrown in your handler, use the underlyingError
property.
Does that address your issue?
So in a scenario like this, how can I throw ValidationsError directly?
struct API: APIProtocol {
func register(_ input: Operations.register.Input) async throws -> Operations.register.Output {
try Components.Schemas.RegisterInput.validate(content: request)
...
}
}
Which module defines ValidationsError
?
You can throw any errors that are convenient to you in the APIProtocol
handlers. By default, those get wrapped in ServerError
, and then how the error is transformed into an HTTP response depends on the specific ServerTransport
(so swift-openapi-vapor
, in your case, which is maintained by the Vapor team).
If you'd like to transform all thrown errors into other errors, you can create a ServerMiddleware
that looks like this:
struct ErrorMappingMiddleware: ServerMiddleware {
func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
metadata: ServerRequestMetadata,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
do {
return try await next(request, body, metadata)
} catch {
if let error = error as? ServerError {
let transformedError = error.underlyingError // here turn the error you threw in your handler into another error
throw transformedError
} else {
let transformedError = error // here turn other errors into your transformed errors
throw transformedError
}
}
}
}
Edit: an end-to-end example of how to use ServerMiddleware: https://github.com/apple/swift-openapi-generator/tree/main/Examples/auth-server-middleware-example
ValidationsError
is defined in Vapor.
Anyway, I added your ErrorMappingMiddleware()
code, called it like this:
let myMiddlewares = [ErrorMappingMiddleware()]
let transport = VaporTransport(routesBuilder: app)
try API().registerHandlers(on: transport, serverURL: Servers.server1(), middlewares: myMiddlewares)
But still errors in that function was thrown as ServerError, I get status code 500 and this as the response:
{
"error": true,
"reason": "Server error - cause description: 'Middleware of type 'ErrorMappingMiddleware' threw an error.', underlying error: The operation couldn’t be completed. (Vapor.ValidationsError error 1.), operationID: ..."
}
Even if the APIProtocol's function is changed to:
func register(_ input: Operations.register.Input) async throws -> Operations.register.Output {
throw Abort(.badRequest, reason: "arbitrary test")
}
Still get back 500 status code and all the "Additional info" in error description.
I tried to extend ServerError
and define description
myself to make it to my liking but it didn't work, the one defined in the swift-openapi-runtime always take precedence.
Before, in vanilla Vapor, I can easily throw an error as a failed response, with the customized status code and purely cusomizable description, now adding openapi generator makes all that harder.
Can you provide the implementation of the middleware? How are you transforming the errors? That's what affects the responses you're seeing.
I pasted in the above ErrorMappingMiddleware
without changing anything, then in entrypoint.swift
file
import Vapor
import Logging
import OpenAPIVapor
@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try configLogger(env: &env)
let app = Application(env)
let transport = VaporTransport(routesBuilder: app)
try API().registerHandlers(on: transport, serverURL: Servers.server1(), middlewares: [ErrorMappingMiddleware()])
defer { app.shutdown() }
do {
try await configure(app)
} catch {
app.logger.report(error: error)
throw error
}
try await app.execute()
}
}
Then in the API() struct, the function does nothing but throw an AbortError defined by Vapor https://docs.vapor.codes/basics/errors/?h=abort#abort-error.
func register(_ input: Operations.register.Input) async throws -> Operations.register.Output {
throw Abort(.badRequest, reason: "test")
}
And the response:
~In the snippet I provided, see the lines // here turn the error into...
- those are the lines where you need to transform your errors into whatever you want them to be 🙂 It was meant to be a snippet that you modify.~
Edit: see below.
@cantbetrue Actually, I just played with this a bit more and realized that while the middleware unwraps the handler's ServerError, the rethrown error gets wrapped again as "middleware threw an error" (because only ServerErrors are passed up without being wrapped).
What this means is that to achieve what you want, you'll need to turn the ErrorMappingMiddleware
from being an OpenAPIRuntime.ServerMiddleware
into a Vapor middleware. That will be called after both the handler and all ServerMiddlewares have been run, and Vapor shouldn't wrap it again - so that's where you can do the final customization before being turned into an HTTP response.
Ok, confirmed that this does what you want:
import OpenAPIRuntime
import OpenAPIVapor
import Vapor
struct Handler: APIProtocol {
func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output {
throw Abort(.badRequest, reason: "arbitrary test")
}
}
struct ErrorMappingMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
do {
return try await next.respond(to: request)
} catch {
if let error = error as? ServerError {
let transformedError = error.underlyingError // here turn the error you threw in your handler into another error
throw transformedError
} else {
let transformedError = error // here turn other errors into your transformed errors
throw transformedError
}
}
}
}
@main struct HelloWorldVaporServer {
static func main() async throws {
let app = Vapor.Application()
app.middleware.use(ErrorMappingMiddleware())
let transport = VaporTransport(routesBuilder: app)
let handler = Handler()
try handler.registerHandlers(on: transport, serverURL: URL(string: "/api")!)
try await app.execute()
}
}
Motivation
Is there anyway I can throw errors without being converted to an ServerError in Vapor project? What's the easiest way to do it for every
APIProtocol
function?I want to use this library to generate code, but it limits how I throw errors, it's frustrating
Proposed solution
Maybe put the conversion to ServerError and ClientError to a middleware and give users the choice whether to use it?
Alternatives considered
No response
Additional information
No response