Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
// Body
case missingRequiredRequestBody
case missingRequiredResponseBody
case failedToParseRequest(DecodingError)

// Multipart
case missingRequiredMultipartFormDataContentType
Expand All @@ -72,6 +73,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
var underlyingError: (any Error)? {
switch self {
case .transportFailed(let error), .handlerFailed(let error), .middlewareFailed(_, let error): return error
case .failedToParseRequest(let decodingError): return decodingError
default: return nil
}
}
Expand Down Expand Up @@ -119,6 +121,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
return "Unexpected response, expected status code: \(expectedStatus), response: \(response)"
case .unexpectedResponseBody(let expectedContentType, let body):
return "Unexpected response body, expected content type: \(expectedContentType), body: \(body)"
case .failedToParseRequest(let decodingError):
return "An error occurred while attempting to parse the request: \(decodingError.prettyDescription)."
}
}

Expand Down Expand Up @@ -160,7 +164,7 @@ extension RuntimeError: HTTPResponseConvertible {
.invalidHeaderFieldName, .malformedAcceptHeader, .missingMultipartBoundaryContentTypeParameter,
.missingOrMalformedContentDispositionName, .missingRequiredHeaderField,
.missingRequiredMultipartFormDataContentType, .missingRequiredQueryParameter, .missingRequiredPathParameter,
.missingRequiredRequestBody, .unsupportedParameterStyle:
.missingRequiredRequestBody, .unsupportedParameterStyle, .failedToParseRequest:
.badRequest
case .handlerFailed, .middlewareFailed, .missingRequiredResponseBody, .transportFailed,
.unexpectedResponseStatus, .unexpectedResponseBody:
Expand Down
70 changes: 69 additions & 1 deletion Sources/OpenAPIRuntime/Errors/ServerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import HTTPTypes
import protocol Foundation.LocalizedError

/// An error thrown by a server handling an OpenAPI operation.
public struct ServerError: Error {
public struct ServerError: Error, HTTPResponseConvertible {

/// Identifier of the operation that threw the error.
public var operationID: String
Expand Down Expand Up @@ -47,6 +47,15 @@ public struct ServerError: Error {
/// The underlying error that caused the operation to fail.
public var underlyingError: any Error

/// An HTTP status to return in the response.
public var httpStatus: HTTPResponse.Status

/// The HTTP header fields of the response.
public var httpHeaderFields: HTTPTypes.HTTPFields

/// The body of the HTTP response.
public var httpBody: OpenAPIRuntime.HTTPBody?

/// Creates a new error.
/// - Parameters:
/// - operationID: The OpenAPI operation identifier.
Expand All @@ -68,6 +77,62 @@ public struct ServerError: Error {
operationOutput: (any Sendable)? = nil,
causeDescription: String,
underlyingError: any Error
) {
let httpStatus: HTTPResponse.Status
let httpHeaderFields: HTTPTypes.HTTPFields
let httpBody: OpenAPIRuntime.HTTPBody?
if let httpConvertibleError = underlyingError as? (any HTTPResponseConvertible) {
httpStatus = httpConvertibleError.httpStatus
httpHeaderFields = httpConvertibleError.httpHeaderFields
httpBody = httpConvertibleError.httpBody
} else {
httpStatus = .internalServerError
httpHeaderFields = [:]
httpBody = nil
}

self.init(
operationID: operationID,
request: request,
requestBody: requestBody,
requestMetadata: requestMetadata,
operationInput: operationInput,
operationOutput: operationOutput,
causeDescription: causeDescription,
underlyingError: underlyingError,
httpStatus: httpStatus,
httpHeaderFields: httpHeaderFields,
httpBody: httpBody
)
}

/// Creates a new error.
/// - Parameters:
/// - operationID: The OpenAPI operation identifier.
/// - request: The HTTP request provided to the server.
/// - requestBody: The HTTP request body provided to the server.
/// - requestMetadata: The request metadata extracted by the server.
/// - operationInput: An operation-specific Input value.
/// - operationOutput: An operation-specific Output value.
/// - causeDescription: A user-facing description of what caused
/// the underlying error to be thrown.
/// - underlyingError: The underlying error that caused the operation
/// to fail.
/// - httpStatus: An HTTP status to return in the response.
/// - httpHeaderFields: The HTTP header fields of the response.
/// - httpBody: The body of the HTTP response.
public init(
operationID: String,
request: HTTPRequest,
requestBody: HTTPBody?,
requestMetadata: ServerRequestMetadata,
operationInput: (any Sendable)? = nil,
operationOutput: (any Sendable)? = nil,
causeDescription: String,
underlyingError: any Error,
httpStatus: HTTPResponse.Status,
httpHeaderFields: HTTPTypes.HTTPFields,
httpBody: OpenAPIRuntime.HTTPBody?
) {
self.operationID = operationID
self.request = request
Expand All @@ -77,6 +142,9 @@ public struct ServerError: Error {
self.operationOutput = operationOutput
self.causeDescription = causeDescription
self.underlyingError = underlyingError
self.httpStatus = httpStatus
self.httpHeaderFields = httpHeaderFields
self.httpBody = httpBody
}

// MARK: Private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,10 @@ public struct ErrorHandlingMiddleware: ServerMiddleware {
async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
do { return try await next(request, body, metadata) } catch {
if let serverError = error as? ServerError,
let appError = serverError.underlyingError as? (any HTTPResponseConvertible)
{
if let serverError = error as? ServerError {
return (
HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
appError.httpBody
HTTPResponse(status: serverError.httpStatus, headerFields: serverError.httpHeaderFields),
serverError.httpBody
)
} else {
return (HTTPResponse(status: .internalServerError), nil)
Expand Down
26 changes: 24 additions & 2 deletions Sources/OpenAPIRuntime/Interface/UniversalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ import struct Foundation.URLComponents
causeDescription = "Unknown"
underlyingError = error
}

let httpStatus: HTTPResponse.Status
let httpHeaderFields: HTTPTypes.HTTPFields
let httpBody: OpenAPIRuntime.HTTPBody?
if let httpConvertibleError = underlyingError as? (any HTTPResponseConvertible) {
httpStatus = httpConvertibleError.httpStatus
httpHeaderFields = httpConvertibleError.httpHeaderFields
httpBody = httpConvertibleError.httpBody
} else if let httpConvertibleError = error as? (any HTTPResponseConvertible) {
httpStatus = httpConvertibleError.httpStatus
httpHeaderFields = httpConvertibleError.httpHeaderFields
httpBody = httpConvertibleError.httpBody
} else {
httpStatus = .internalServerError
httpHeaderFields = [:]
httpBody = nil
}
return ServerError(
operationID: operationID,
request: request,
Expand All @@ -127,13 +144,18 @@ import struct Foundation.URLComponents
operationInput: input,
operationOutput: output,
causeDescription: causeDescription,
underlyingError: underlyingError
underlyingError: underlyingError,
httpStatus: httpStatus,
httpHeaderFields: httpHeaderFields,
httpBody: httpBody
)
}
var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) =
{ _request, _requestBody, _metadata in
let input: OperationInput = try await wrappingErrors {
try await deserializer(_request, _requestBody, _metadata)
do { return try await deserializer(_request, _requestBody, _metadata) } catch let decodingError
as DecodingError
{ throw RuntimeError.failedToParseRequest(decodingError) }
} mapError: { error in
makeError(error: error)
}
Expand Down
5 changes: 4 additions & 1 deletion Tests/OpenAPIRuntimeTests/Errors/Test_ClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ final class Test_ServerError: XCTestCase {
requestBody: nil,
requestMetadata: .init(),
causeDescription: upstreamError.prettyDescription,
underlyingError: upstreamError.underlyingError ?? upstreamError
underlyingError: upstreamError.underlyingError ?? upstreamError,
httpStatus: .internalServerError,
httpHeaderFields: [:],
httpBody: nil
)
XCTAssertEqual(
"\(error)",
Expand Down
31 changes: 31 additions & 0 deletions Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,37 @@ final class Test_UniversalServer: Test_Runtime {
}
}

func testErrorPropagation_deserializerWithDecodingError() async throws {
let decodingError = DecodingError.dataCorrupted(
.init(codingPath: [], debugDescription: "Invalid request body.")
)
do {
let server = UniversalServer(handler: MockHandler())
_ = try await server.handle(
request: .init(soar_path: "/", method: .post),
requestBody: MockHandler.requestBody,
metadata: .init(),
forOperation: "op",
using: { MockHandler.greet($0) },
deserializer: { request, body, metadata in throw decodingError },
serializer: { output, _ in fatalError() }
)
} catch {
let serverError = try XCTUnwrap(error as? ServerError)
XCTAssertEqual(serverError.operationID, "op")
XCTAssert(serverError.causeDescription.contains("An error occurred while attempting to parse the request"))
XCTAssert(serverError.underlyingError is DecodingError)
XCTAssertEqual(serverError.httpStatus, .badRequest)
XCTAssertEqual(serverError.httpHeaderFields, [:])
XCTAssertNil(serverError.httpBody)
XCTAssertEqual(serverError.request, .init(soar_path: "/", method: .post))
XCTAssertEqual(serverError.requestBody, MockHandler.requestBody)
XCTAssertEqual(serverError.requestMetadata, .init())
XCTAssertNil(serverError.operationInput)
XCTAssertNil(serverError.operationOutput)
}
}

func testErrorPropagation_handler() async throws {
do {
let server = UniversalServer(handler: MockHandler(shouldFail: true))
Expand Down
Loading