Skip to content

Commit 1a38260

Browse files
winnisx7claude
andcommitted
Add centralized error handling through Configuration
This introduces ErrorHandler protocols that allow customization of error handling for both client and server operations, enabling centralized error management, logging, and monitoring capabilities. ## Changes ### New Error Handler Protocols - Add `ClientErrorHandler` protocol for client-side error handling - Add `ServerErrorHandler` protocol for server-side error handling - Add `ClientErrorContext` and `ServerErrorContext` for rich error context - Provide `DefaultClientErrorHandler` and `DefaultServerErrorHandler` as backward-compatible default implementations ### Configuration Integration - Add `clientErrorHandler` property to `Configuration` (default: `DefaultClientErrorHandler()`) - Add `serverErrorHandler` property to `Configuration` (default: `DefaultServerErrorHandler()`) - Both properties have default values, ensuring zero breaking changes ### Runtime Integration - Update `UniversalClient` to use configured `ClientErrorHandler` - Update `UniversalServer` to use configured `ServerErrorHandler` - All errors now flow through the configured handlers, providing a single interception point for logging, monitoring, and transformation ### Testing - Add comprehensive unit tests for both error handlers (11 tests) - Add integration tests for end-to-end error handling flow (9 tests) - All 226 existing tests continue to pass ## Usage Example ```swift // Custom error handler with logging struct LoggingClientErrorHandler: ClientErrorHandler { func handleClientError(_ error: any Error, context: ClientErrorContext) -> any Error { // Add logging/monitoring logger.error("Client error in \(context.operationID): \(error)") // Use default transformation return DefaultClientErrorHandler().handleClientError(error, context: context) } } // Configure client with custom handler let config = Configuration( clientErrorHandler: LoggingClientErrorHandler() ) let client = UniversalClient(configuration: config, transport: transport) ``` ## Benefits - **Backward Compatible**: Default handlers maintain existing behavior - **Centralized**: Single interception point for all errors - **Extensible**: Easy to add logging, monitoring, or custom transformations - **Type-Safe**: Protocol-based design with compile-time verification - **Rich Context**: Full operation context available at error handling time 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7cdf333 commit 1a38260

File tree

6 files changed

+1003
-60
lines changed

6 files changed

+1003
-60
lines changed

Sources/OpenAPIRuntime/Conversion/Configuration.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ public struct Configuration: Sendable {
152152
/// Custom XML coder for encoding and decoding xml bodies.
153153
public var xmlCoder: (any CustomCoder)?
154154

155+
/// The handler for client-side errors.
156+
///
157+
/// This handler is invoked whenever an error occurs during client operations
158+
/// (request serialization, transport, or response deserialization).
159+
/// Customize this to add logging, monitoring, or custom error transformation.
160+
public var clientErrorHandler: any ClientErrorHandler
161+
162+
/// The handler for server-side errors.
163+
///
164+
/// This handler is invoked whenever an error occurs during server operations
165+
/// (request deserialization, handler execution, or response serialization).
166+
/// Customize this to add logging, monitoring, or custom error transformation.
167+
public var serverErrorHandler: any ServerErrorHandler
168+
155169
/// Creates a new configuration with the specified values.
156170
///
157171
/// - Parameters:
@@ -160,15 +174,21 @@ public struct Configuration: Sendable {
160174
/// - jsonEncodingOptions: The options for the underlying JSON encoder.
161175
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
162176
/// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads.
177+
/// - clientErrorHandler: The handler for client-side errors. Defaults to ``DefaultClientErrorHandler``.
178+
/// - serverErrorHandler: The handler for server-side errors. Defaults to ``DefaultServerErrorHandler``.
163179
public init(
164180
dateTranscoder: any DateTranscoder = .iso8601,
165181
jsonEncodingOptions: JSONEncodingOptions = [.sortedKeys, .prettyPrinted],
166182
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
167-
xmlCoder: (any CustomCoder)? = nil
183+
xmlCoder: (any CustomCoder)? = nil,
184+
clientErrorHandler: any ClientErrorHandler = DefaultClientErrorHandler(),
185+
serverErrorHandler: any ServerErrorHandler = DefaultServerErrorHandler()
168186
) {
169187
self.dateTranscoder = dateTranscoder
170188
self.jsonEncodingOptions = jsonEncodingOptions
171189
self.multipartBoundaryGenerator = multipartBoundaryGenerator
172190
self.xmlCoder = xmlCoder
191+
self.clientErrorHandler = clientErrorHandler
192+
self.serverErrorHandler = serverErrorHandler
173193
}
174194
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import Foundation
15+
import HTTPTypes
16+
17+
// MARK: - Client Error Handler
18+
19+
/// A protocol for handling errors that occur during client operations.
20+
///
21+
/// Implement this protocol to customize how client-side errors are processed,
22+
/// logged, and transformed before being thrown to the caller.
23+
public protocol ClientErrorHandler: Sendable {
24+
/// Handles an error that occurred during a client operation.
25+
///
26+
/// This method is called whenever an error occurs during request serialization,
27+
/// transport, or response deserialization in the client.
28+
///
29+
/// - Parameters:
30+
/// - error: The original error that was thrown.
31+
/// - context: Context information about the operation and request/response.
32+
/// - Returns: An error to be thrown to the caller. This can be the original error,
33+
/// a transformed error, or a new error based on your error handling logic.
34+
func handleClientError(
35+
_ error: any Error,
36+
context: ClientErrorContext
37+
) -> any Error
38+
}
39+
40+
/// Context information for client error handling.
41+
public struct ClientErrorContext: Sendable {
42+
/// The operation identifier.
43+
public let operationID: String
44+
45+
/// The operation input that was being sent.
46+
public let operationInput: (any Sendable)?
47+
48+
/// The HTTP request, if it was created before the error occurred.
49+
public let request: HTTPRequest?
50+
51+
/// The HTTP request body, if it was created before the error occurred.
52+
public let requestBody: HTTPBody?
53+
54+
/// The base URL used for the request.
55+
public let baseURL: URL?
56+
57+
/// The HTTP response, if it was received before the error occurred.
58+
public let response: HTTPResponse?
59+
60+
/// The HTTP response body, if it was received before the error occurred.
61+
public let responseBody: HTTPBody?
62+
63+
/// Creates a new client error context.
64+
public init(
65+
operationID: String,
66+
operationInput: (any Sendable)? = nil,
67+
request: HTTPRequest? = nil,
68+
requestBody: HTTPBody? = nil,
69+
baseURL: URL? = nil,
70+
response: HTTPResponse? = nil,
71+
responseBody: HTTPBody? = nil
72+
) {
73+
self.operationID = operationID
74+
self.operationInput = operationInput
75+
self.request = request
76+
self.requestBody = requestBody
77+
self.baseURL = baseURL
78+
self.response = response
79+
self.responseBody = responseBody
80+
}
81+
}
82+
83+
/// The default client error handler implementation.
84+
///
85+
/// This handler wraps errors in a ``ClientError`` struct that includes
86+
/// context information about the operation, request, and response.
87+
public struct DefaultClientErrorHandler: ClientErrorHandler {
88+
/// Creates a new default client error handler.
89+
public init() {}
90+
91+
public func handleClientError(
92+
_ error: any Error,
93+
context: ClientErrorContext
94+
) -> any Error {
95+
// If already a ClientError, update it with missing context
96+
if var clientError = error as? ClientError {
97+
clientError.request = clientError.request ?? context.request
98+
clientError.requestBody = clientError.requestBody ?? context.requestBody
99+
clientError.baseURL = clientError.baseURL ?? context.baseURL
100+
clientError.response = clientError.response ?? context.response
101+
clientError.responseBody = clientError.responseBody ?? context.responseBody
102+
return clientError
103+
}
104+
105+
// Extract description and underlying error
106+
let causeDescription: String
107+
let underlyingError: any Error
108+
if let runtimeError = error as? RuntimeError {
109+
causeDescription = runtimeError.prettyDescription
110+
underlyingError = runtimeError.underlyingError ?? error
111+
} else {
112+
causeDescription = "Unknown"
113+
underlyingError = error
114+
}
115+
116+
// Create new ClientError with full context
117+
return ClientError(
118+
operationID: context.operationID,
119+
operationInput: context.operationInput,
120+
request: context.request,
121+
requestBody: context.requestBody,
122+
baseURL: context.baseURL,
123+
response: context.response,
124+
responseBody: context.responseBody,
125+
causeDescription: causeDescription,
126+
underlyingError: underlyingError
127+
)
128+
}
129+
}
130+
131+
// MARK: - Server Error Handler
132+
133+
/// A protocol for handling errors that occur during server operations.
134+
///
135+
/// Implement this protocol to customize how server-side errors are processed,
136+
/// logged, and transformed into HTTP responses.
137+
public protocol ServerErrorHandler: Sendable {
138+
/// Handles an error that occurred during a server operation.
139+
///
140+
/// This method is called whenever an error occurs during request deserialization,
141+
/// handler execution, or response serialization in the server.
142+
///
143+
/// - Parameters:
144+
/// - error: The original error that was thrown.
145+
/// - context: Context information about the operation and request/response.
146+
/// - Returns: An error to be thrown to the middleware. This can be the original error,
147+
/// a transformed error, or a new error based on your error handling logic.
148+
func handleServerError(
149+
_ error: any Error,
150+
context: ServerErrorContext
151+
) -> any Error
152+
}
153+
154+
/// Context information for server error handling.
155+
public struct ServerErrorContext: Sendable {
156+
/// The operation identifier.
157+
public let operationID: String
158+
159+
/// The HTTP request that was received.
160+
public let request: HTTPRequest
161+
162+
/// The HTTP request body that was received.
163+
public let requestBody: HTTPBody?
164+
165+
/// Metadata about the server request.
166+
public let requestMetadata: ServerRequestMetadata
167+
168+
/// The operation input, if it was successfully deserialized before the error occurred.
169+
public let operationInput: (any Sendable)?
170+
171+
/// The operation output, if it was produced before the error occurred.
172+
public let operationOutput: (any Sendable)?
173+
174+
/// Creates a new server error context.
175+
public init(
176+
operationID: String,
177+
request: HTTPRequest,
178+
requestBody: HTTPBody? = nil,
179+
requestMetadata: ServerRequestMetadata,
180+
operationInput: (any Sendable)? = nil,
181+
operationOutput: (any Sendable)? = nil
182+
) {
183+
self.operationID = operationID
184+
self.request = request
185+
self.requestBody = requestBody
186+
self.requestMetadata = requestMetadata
187+
self.operationInput = operationInput
188+
self.operationOutput = operationOutput
189+
}
190+
}
191+
192+
/// The default server error handler implementation.
193+
///
194+
/// This handler wraps errors in a ``ServerError`` struct that includes
195+
/// context information about the operation, request, and the HTTP response to be sent.
196+
public struct DefaultServerErrorHandler: ServerErrorHandler {
197+
/// Creates a new default server error handler.
198+
public init() {}
199+
200+
public func handleServerError(
201+
_ error: any Error,
202+
context: ServerErrorContext
203+
) -> any Error {
204+
// If already a ServerError, update it with missing context
205+
if var serverError = error as? ServerError {
206+
serverError.operationInput = serverError.operationInput ?? context.operationInput
207+
serverError.operationOutput = serverError.operationOutput ?? context.operationOutput
208+
return serverError
209+
}
210+
211+
// Extract description and underlying error
212+
let causeDescription: String
213+
let underlyingError: any Error
214+
if let runtimeError = error as? RuntimeError {
215+
causeDescription = runtimeError.prettyDescription
216+
underlyingError = runtimeError.underlyingError ?? error
217+
} else {
218+
causeDescription = "Unknown"
219+
underlyingError = error
220+
}
221+
222+
// Determine HTTP response properties
223+
let httpStatus: HTTPResponse.Status
224+
let httpHeaderFields: HTTPTypes.HTTPFields
225+
let httpBody: OpenAPIRuntime.HTTPBody?
226+
227+
if let httpConvertibleError = underlyingError as? (any HTTPResponseConvertible) {
228+
httpStatus = httpConvertibleError.httpStatus
229+
httpHeaderFields = httpConvertibleError.httpHeaderFields
230+
httpBody = httpConvertibleError.httpBody
231+
} else if let httpConvertibleError = error as? (any HTTPResponseConvertible) {
232+
httpStatus = httpConvertibleError.httpStatus
233+
httpHeaderFields = httpConvertibleError.httpHeaderFields
234+
httpBody = httpConvertibleError.httpBody
235+
} else {
236+
httpStatus = .internalServerError
237+
httpHeaderFields = [:]
238+
httpBody = nil
239+
}
240+
241+
// Create new ServerError with full context
242+
return ServerError(
243+
operationID: context.operationID,
244+
request: context.request,
245+
requestBody: context.requestBody,
246+
requestMetadata: context.requestMetadata,
247+
operationInput: context.operationInput,
248+
operationOutput: context.operationOutput,
249+
causeDescription: causeDescription,
250+
underlyingError: underlyingError,
251+
httpStatus: httpStatus,
252+
httpHeaderFields: httpHeaderFields,
253+
httpBody: httpBody
254+
)
255+
}
256+
}

Sources/OpenAPIRuntime/Interface/UniversalClient.swift

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import struct Foundation.URL
9898
}
9999
}
100100
let baseURL = serverURL
101+
let errorHandler = converter.configuration.clientErrorHandler
101102
@Sendable func makeError(
102103
request: HTTPRequest? = nil,
103104
requestBody: HTTPBody? = nil,
@@ -106,34 +107,16 @@ import struct Foundation.URL
106107
responseBody: HTTPBody? = nil,
107108
error: any Error
108109
) -> any Error {
109-
if var error = error as? ClientError {
110-
error.request = error.request ?? request
111-
error.requestBody = error.requestBody ?? requestBody
112-
error.baseURL = error.baseURL ?? baseURL
113-
error.response = error.response ?? response
114-
error.responseBody = error.responseBody ?? responseBody
115-
return error
116-
}
117-
let causeDescription: String
118-
let underlyingError: any Error
119-
if let runtimeError = error as? RuntimeError {
120-
causeDescription = runtimeError.prettyDescription
121-
underlyingError = runtimeError.underlyingError ?? error
122-
} else {
123-
causeDescription = "Unknown"
124-
underlyingError = error
125-
}
126-
return ClientError(
110+
let context = ClientErrorContext(
127111
operationID: operationID,
128112
operationInput: input,
129113
request: request,
130114
requestBody: requestBody,
131115
baseURL: baseURL,
132116
response: response,
133-
responseBody: responseBody,
134-
causeDescription: causeDescription,
135-
underlyingError: underlyingError
117+
responseBody: responseBody
136118
)
119+
return errorHandler.handleClientError(error, context: context)
137120
}
138121
let (request, requestBody): (HTTPRequest, HTTPBody?) = try await wrappingErrors {
139122
try serializer(input)

0 commit comments

Comments
 (0)