Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved enriched error propagation from the transport and middlewares #63

Merged
merged 6 commits into from
Oct 26, 2023
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
84 changes: 84 additions & 0 deletions Sources/OpenAPIRuntime/Deprecated/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,89 @@
//
//===----------------------------------------------------------------------===//
import Foundation
import HTTPTypes

// MARK: - Functionality to be removed in the future

extension ClientError {
/// Creates a new error.
/// - Parameters:
/// - operationID: The OpenAPI operation identifier.
/// - operationInput: The operation-specific Input value.
/// - request: The HTTP request created during the operation.
/// - requestBody: The HTTP request body created during the operation.
/// - baseURL: The base URL for HTTP requests.
/// - response: The HTTP response received during the operation.
/// - responseBody: The HTTP response body received during the operation.
/// - underlyingError: The underlying error that caused the operation
/// to fail.
@available(
*,
deprecated,
renamed:
"ClientError.init(operationID:operationInput:request:requestBody:baseURL:response:responseBody:causeDescription:underlyingError:)",
message: "Use the initializer with a causeDescription parameter."
)
public init(
operationID: String,
operationInput: any Sendable,
request: HTTPRequest? = nil,
requestBody: HTTPBody? = nil,
baseURL: URL? = nil,
response: HTTPResponse? = nil,
responseBody: HTTPBody? = nil,
underlyingError: any Error
) {
self.init(
operationID: operationID,
operationInput: operationInput,
request: request,
requestBody: requestBody,
baseURL: baseURL,
response: response,
responseBody: responseBody,
causeDescription: "Legacy error without a causeDescription.",
underlyingError: underlyingError
)
}
}

extension ServerError {
/// 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.
/// - underlyingError: The underlying error that caused the operation
/// to fail.
@available(
*,
deprecated,
renamed:
"ServerError.init(operationID:request:requestBody:requestMetadata:operationInput:operationOutput:causeDescription:underlyingError:)",
message: "Use the initializer with a causeDescription parameter."
)
public init(
operationID: String,
request: HTTPRequest,
requestBody: HTTPBody?,
requestMetadata: ServerRequestMetadata,
operationInput: (any Sendable)? = nil,
operationOutput: (any Sendable)? = nil,
underlyingError: any Error
) {
self.init(
operationID: operationID,
request: request,
requestBody: requestBody,
requestMetadata: requestMetadata,
operationInput: operationInput,
operationOutput: operationOutput,
causeDescription: "Legacy error without a causeDescription.",
underlyingError: underlyingError
)
}
}
10 changes: 9 additions & 1 deletion Sources/OpenAPIRuntime/Errors/ClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public struct ClientError: Error {
/// Will be nil if the error resulted before the response was received.
public var responseBody: HTTPBody?

/// A user-facing description of what caused the underlying error
/// to be thrown.
public var causeDescription: String
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved

/// The underlying error that caused the operation to fail.
public var underlyingError: any Error

Expand All @@ -76,6 +80,8 @@ public struct ClientError: Error {
/// - baseURL: The base URL for HTTP requests.
/// - response: The HTTP response received during the operation.
/// - responseBody: The HTTP response body received during the operation.
/// - causeDescription: A user-facing description of what caused
/// the underlying error to be thrown.
/// - underlyingError: The underlying error that caused the operation
/// to fail.
public init(
Expand All @@ -86,6 +92,7 @@ public struct ClientError: Error {
baseURL: URL? = nil,
response: HTTPResponse? = nil,
responseBody: HTTPBody? = nil,
causeDescription: String,
underlyingError: any Error
) {
self.operationID = operationID
Expand All @@ -95,6 +102,7 @@ public struct ClientError: Error {
self.baseURL = baseURL
self.response = response
self.responseBody = responseBody
self.causeDescription = causeDescription
self.underlyingError = underlyingError
}

Expand All @@ -115,7 +123,7 @@ extension ClientError: CustomStringConvertible {
///
/// - Returns: A string describing the client error and its associated details.
public var description: String {
"Client error - operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.prettyDescription ?? "<nil>"), requestBody: \(requestBody?.prettyDescription ?? "<nil>"), baseURL: \(baseURL?.absoluteString ?? "<nil>"), response: \(response?.prettyDescription ?? "<nil>"), responseBody: \(responseBody?.prettyDescription ?? "<nil>") , underlying error: \(underlyingErrorDescription)"
"Client error - cause description: '\(causeDescription)', underlying error: \(underlyingErrorDescription), operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.prettyDescription ?? "<nil>"), requestBody: \(requestBody?.prettyDescription ?? "<nil>"), baseURL: \(baseURL?.absoluteString ?? "<nil>"), response: \(response?.prettyDescription ?? "<nil>"), responseBody: \(responseBody?.prettyDescription ?? "<nil>")"
}
}

Expand Down
23 changes: 19 additions & 4 deletions Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,25 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret

// Transport/Handler
case transportFailed(any Error)
case middlewareFailed(middlewareType: Any.Type, any Error)
case handlerFailed(any Error)

// Unexpected response (thrown by shorthand APIs)
case unexpectedResponseStatus(expectedStatus: String, response: any Sendable)
case unexpectedResponseBody(expectedContent: String, body: any Sendable)

/// A wrapped root cause error, if one was thrown by other code.
var underlyingError: (any Error)? {
switch self {
case .transportFailed(let error),
.handlerFailed(let error),
.middlewareFailed(_, let error):
return error
default:
return nil
}
}

// MARK: CustomStringConvertible

var description: String {
Expand Down Expand Up @@ -103,10 +116,12 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
return "Missing required request body"
case .missingRequiredResponseBody:
return "Missing required response body"
case .transportFailed(let underlyingError):
return "Transport failed with error: \(underlyingError.localizedDescription)"
case .handlerFailed(let underlyingError):
return "User handler failed with error: \(underlyingError.localizedDescription)"
case .transportFailed:
return "Transport threw an error."
case .middlewareFailed(middlewareType: let type, _):
return "Middleware of type '\(type)' threw an error."
case .handlerFailed:
return "User handler threw an error."
case .unexpectedResponseStatus(let expectedStatus, let response):
return "Unexpected response, expected status code: \(expectedStatus), response: \(response)"
case .unexpectedResponseBody(let expectedContentType, let body):
Expand Down
12 changes: 10 additions & 2 deletions Sources/OpenAPIRuntime/Errors/ServerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public struct ServerError: Error {
/// Is nil if error was thrown before/during Output -> response conversion.
public var operationOutput: (any Sendable)?

/// A user-facing description of what caused the underlying error
/// to be thrown.
public var causeDescription: String

/// The underlying error that caused the operation to fail.
public var underlyingError: any Error

Expand All @@ -51,6 +55,8 @@ public struct ServerError: Error {
/// - 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.
public init(
Expand All @@ -60,14 +66,16 @@ public struct ServerError: Error {
requestMetadata: ServerRequestMetadata,
operationInput: (any Sendable)? = nil,
operationOutput: (any Sendable)? = nil,
underlyingError: (any Error)
causeDescription: String,
underlyingError: any Error
) {
self.operationID = operationID
self.request = request
self.requestBody = requestBody
self.requestMetadata = requestMetadata
self.operationInput = operationInput
self.operationOutput = operationOutput
self.causeDescription = causeDescription
self.underlyingError = underlyingError
}

Expand All @@ -88,7 +96,7 @@ extension ServerError: CustomStringConvertible {
///
/// - Returns: A string describing the server error and its associated details.
public var description: String {
"Server error - operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? "<nil>"), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? "<nil>"), operationOutput: \(operationOutput.map { String(describing: $0) } ?? "<nil>"), underlying error: \(underlyingErrorDescription)"
"Server error - cause description: '\(causeDescription)', underlying error: \(underlyingErrorDescription), operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? "<nil>"), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? "<nil>"), operationOutput: \(operationOutput.map { String(describing: $0) } ?? "<nil>")"
}
}

Expand Down
90 changes: 65 additions & 25 deletions Sources/OpenAPIRuntime/Interface/UniversalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,76 +90,116 @@ import Foundation
serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?),
deserializer: @Sendable (HTTPResponse, HTTPBody?) async throws -> OperationOutput
) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable {
@Sendable
func wrappingErrors<R>(
@Sendable func wrappingErrors<R>(
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
work: () async throws -> R,
mapError: (any Error) -> any Error
) async throws -> R {
do {
return try await work()
} catch let error as ClientError {
throw error
} catch {
throw mapError(error)
}
}
let baseURL = serverURL
func makeError(
@Sendable func makeError(
request: HTTPRequest? = nil,
requestBody: HTTPBody? = nil,
baseURL: URL? = nil,
response: HTTPResponse? = nil,
responseBody: HTTPBody? = nil,
error: any Error
) -> any Error {
ClientError(
if var error = error as? ClientError {
error.request = error.request ?? request
error.requestBody = error.requestBody ?? requestBody
error.baseURL = error.baseURL ?? baseURL
error.response = error.response ?? response
error.responseBody = error.responseBody ?? responseBody
return error
}
let causeDescription: String
let underlyingError: any Error
if let runtimeError = error as? RuntimeError {
causeDescription = runtimeError.prettyDescription
underlyingError = runtimeError.underlyingError ?? error
} else {
causeDescription = "Unknown"
underlyingError = error
}
return ClientError(
operationID: operationID,
operationInput: input,
request: request,
requestBody: requestBody,
baseURL: baseURL,
response: response,
responseBody: responseBody,
underlyingError: error
causeDescription: causeDescription,
underlyingError: underlyingError
)
}
let (request, requestBody): (HTTPRequest, HTTPBody?) = try await wrappingErrors {
try serializer(input)
} mapError: { error in
makeError(error: error)
}
let (response, responseBody): (HTTPResponse, HTTPBody?) = try await wrappingErrors {
var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) = {
var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) = {
(_request, _body, _url) in
try await wrappingErrors {
try await transport.send(
_request,
body: _body,
baseURL: _url,
operationID: operationID
)
} mapError: { error in
makeError(
request: request,
requestBody: requestBody,
baseURL: baseURL,
error: RuntimeError.transportFailed(error)
)
}
}
for middleware in middlewares.reversed() {
let tmp = next
next = {
(_request, _body, _url) in
try await wrappingErrors {
try await transport.send(
try await middleware.intercept(
_request,
body: _body,
baseURL: _url,
operationID: operationID
)
} mapError: { error in
RuntimeError.transportFailed(error)
}
}
for middleware in middlewares.reversed() {
let tmp = next
next = {
try await middleware.intercept(
$0,
body: $1,
baseURL: $2,
operationID: operationID,
next: tmp
)
} mapError: { error in
makeError(
request: request,
requestBody: requestBody,
baseURL: baseURL,
error: RuntimeError.middlewareFailed(
middlewareType: type(of: middleware),
error
)
)
}
}
return try await next(request, requestBody, baseURL)
} mapError: { error in
makeError(request: request, baseURL: baseURL, error: error)
}
let (response, responseBody): (HTTPResponse, HTTPBody?) = try await next(request, requestBody, baseURL)
return try await wrappingErrors {
try await deserializer(response, responseBody)
} mapError: { error in
makeError(request: request, baseURL: baseURL, response: response, error: error)
makeError(
request: request,
requestBody: requestBody,
baseURL: baseURL,
response: response,
responseBody: responseBody,
error: error
)
}
}
}
Loading