Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
57 changes: 56 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,49 @@ public struct ServerError: Error {
operationOutput: (any Sendable)? = nil,
causeDescription: String,
underlyingError: any Error
) {
self.init(
operationID: operationID,
request: request,
requestBody: requestBody,
requestMetadata: requestMetadata,
operationInput: operationInput,
operationOutput: operationOutput,
causeDescription: causeDescription,
underlyingError: underlyingError,
httpStatus: .internalServerError,
httpHeaderFields: [:],
httpBody: nil
)
}

/// 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 +129,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
23 changes: 14 additions & 9 deletions Sources/OpenAPIRuntime/Interface/ErrorHandlingMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,21 @@ 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)
{
return (
HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
appError.httpBody
)
} else {
return (HTTPResponse(status: .internalServerError), nil)
if let serverError = error as? ServerError {
if let appError = serverError.underlyingError as? (any HTTPResponseConvertible) {
return (
HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
appError.httpBody
)
} else {
return (
HTTPResponse(status: serverError.httpStatus, headerFields: serverError.httpHeaderFields),
serverError.httpBody
)
}
}

return (HTTPResponse(status: .internalServerError), nil)
}
}
}
Expand Down
21 changes: 19 additions & 2 deletions Sources/OpenAPIRuntime/Interface/UniversalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,22 @@ import struct Foundation.URLComponents
}
let causeDescription: String
let underlyingError: any Error
let httpStatus: HTTPResponse.Status
let httpHeaderFields: HTTPTypes.HTTPFields
let httpBody: OpenAPIRuntime.HTTPBody?
if let runtimeError = error as? RuntimeError {
causeDescription = runtimeError.prettyDescription
underlyingError = runtimeError.underlyingError ?? error
httpStatus = runtimeError.httpStatus
httpHeaderFields = runtimeError.httpHeaderFields
httpBody = runtimeError.httpBody

} else {
causeDescription = "Unknown"
underlyingError = error
httpStatus = .internalServerError
httpHeaderFields = [:]
httpBody = nil
}
return ServerError(
operationID: operationID,
Expand All @@ -127,13 +137,20 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ struct MockErrorMiddleware_Next: ServerMiddleware {
requestBody: body,
requestMetadata: metadata,
causeDescription: "",
underlyingError: underlyingError
underlyingError: underlyingError,
httpStatus: .internalServerError,
httpHeaderFields: [:],
httpBody: nil
)
}
let (response, responseBody) = try await next(request, body, metadata)
Expand Down
29 changes: 29 additions & 0 deletions Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,35 @@ 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