diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0015.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0015.md new file mode 100644 index 00000000..85ff8a64 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0015.md @@ -0,0 +1,377 @@ +# SOAR-0015: Error Handler Protocols for Client and Server + +Introduce `ClientErrorHandler` and `ServerErrorHandler` protocols for centralized error observation on both client and server sides. + +## Overview + +- Proposal: SOAR-0015 +- Author(s): [winnisx7](https://github.com/winnisx7) +- Status: **Proposed** +- Issue: [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162) +- Implementation: + - [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162) +- Affected components: + - runtime + +### Introduction + +This proposal introduces `ClientErrorHandler` and `ServerErrorHandler` protocols to provide extension points for centralized error observation on both client and server sides. These handlers are configured through the `Configuration` struct and are invoked after errors have been wrapped in `ClientError` or `ServerError`. + +### Motivation + +Currently, swift-openapi-runtime provides limited options for centralized error handling. Developers face the following challenges: + +**Problem 1: Scattered Error Handling** + +Errors must be handled individually at each API call site, leading to code duplication and inconsistent error handling. + +```swift +// Current approach: individual handling at each call site +do { + let response = try await client.getUser(...) +} catch { + // Repeated error handling logic + logger.error("API error: \(error)") +} + +do { + let response = try await client.getPosts(...) +} catch { + // Same error handling logic repeated + logger.error("API error: \(error)") +} +``` + +**Problem 2: Middleware Limitations** + +The existing `ClientMiddleware` operates at the HTTP request/response level, making it difficult to intercept decoding errors or runtime errors. There is still a lack of extension points for error **observation**. + +**Problem 3: Telemetry and Logging Complexity** + +To collect telemetry or implement centralized logging for all errors, developers currently need to modify every API call site. + +**Problem 4: Difficulty Utilizing Error Context** + +`ClientError` and `ServerError` contain rich context information such as `operationID`, `request`, and `response`, but centralized analysis using this information is difficult. + +### Proposed solution + +Introduce `ClientErrorHandler` and `ServerErrorHandler` protocols and add optional handler properties to the `Configuration` struct. These handlers are invoked **after** errors have been wrapped in `ClientError` or `ServerError`, allowing logging, monitoring, and analytics operations. + +```swift +// Custom error handler with logging +struct LoggingClientErrorHandler: ClientErrorHandler { + func handleClientError(_ error: ClientError) { + logger.error("Client error in \(error.operationID): \(error.causeDescription)") + analytics.track("client_error", metadata: [ + "operation": error.operationID, + "status": error.response?.status.code + ]) + } +} + +let config = Configuration( + clientErrorHandler: LoggingClientErrorHandler() +) +let client = UniversalClient(configuration: config, transport: transport) +``` + +### Detailed design + +#### New Protocol Definitions + +```swift +/// A protocol for handling client-side errors. +/// +/// Implement this protocol to observe and react to errors that occur during +/// client API calls. The handler is invoked after the error has been wrapped +/// in a ``ClientError``. +/// +/// Use this to add logging, monitoring, or analytics for client-side errors. +public protocol ClientErrorHandler: Sendable { + /// Handles a client error. + /// + /// This method is called after an error has been wrapped in a ``ClientError`` + /// but before it is thrown to the caller. + /// + /// - Parameter error: The client error that occurred, containing context such as + /// the operation ID, request, response, and underlying cause. + func handleClientError(_ error: ClientError) +} + +/// A protocol for handling server-side errors. +/// +/// Implement this protocol to observe and react to errors that occur during +/// server request handling. The handler is invoked after the error has been +/// wrapped in a ``ServerError``. +/// +/// Use this to add logging, monitoring, or analytics for server-side errors. +public protocol ServerErrorHandler: Sendable { + /// Handles a server error. + /// + /// This method is called after an error has been wrapped in a ``ServerError`` + /// but before it is thrown to the caller. + /// + /// - Parameter error: The server error that occurred, containing context such as + /// the operation ID, request, and underlying cause. + func handleServerError(_ error: ServerError) +} +``` + +#### Configuration Struct Changes + +```swift +public struct Configuration: Sendable { + // ... existing properties ... + + /// Custom XML coder for encoding and decoding xml bodies. + public var xmlCoder: (any CustomCoder)? + + /// The handler for client-side errors. + /// + /// This handler is invoked after a client error has been wrapped in a ``ClientError``. + /// Use this to add logging, monitoring, or analytics for client-side errors. + /// If `nil`, errors are thrown without additional handling. + public var clientErrorHandler: (any ClientErrorHandler)? + + /// The handler for server-side errors. + /// + /// This handler is invoked after a server error has been wrapped in a ``ServerError``. + /// Use this to add logging, monitoring, or analytics for server-side errors. + /// If `nil`, errors are thrown without additional handling. + public var serverErrorHandler: (any ServerErrorHandler)? + + /// Creates a new configuration with the specified values. + /// + /// - Parameters: + /// - dateTranscoder: The transcoder for date/time conversions. + /// - multipartBoundaryGenerator: The generator for multipart boundaries. + /// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. + /// - clientErrorHandler: Optional handler for observing client-side errors. Defaults to `nil`. + /// - serverErrorHandler: Optional handler for observing server-side errors. Defaults to `nil`. + public init( + dateTranscoder: any DateTranscoder = .iso8601, + multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, + xmlCoder: (any CustomCoder)? = nil, + clientErrorHandler: (any ClientErrorHandler)? = nil, + serverErrorHandler: (any ServerErrorHandler)? = nil + ) { + self.dateTranscoder = dateTranscoder + self.multipartBoundaryGenerator = multipartBoundaryGenerator + self.xmlCoder = xmlCoder + self.clientErrorHandler = clientErrorHandler + self.serverErrorHandler = serverErrorHandler + } +} +``` + +#### UniversalClient Changes + +In `UniversalClient`, the configured handler is called after the error has been wrapped in `ClientError`: + +```swift +// Inside UniversalClient (pseudocode) +do { + // API call logic +} catch { + let clientError = ClientError( + operationID: operationID, + request: request, + response: response, + underlyingError: error + ) + + // Call handler if configured + configuration.clientErrorHandler?.handleClientError(clientError) + + throw clientError +} +``` + +#### UniversalServer Changes + +Similarly, `UniversalServer` calls the handler after wrapping the error in `ServerError`. + +#### Error Handling Flow + +``` +┌──────────────────────┐ +│ API Call/Handle │ +└──────────┬───────────┘ + │ + ▼ (error occurs) +┌──────────────────────┐ +│ Wrap in ClientError/ │ +│ ServerError │ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────┐ +│ errorHandler │──────┐ +│ configured? │ │ No +└──────────┬───────────┘ │ + Yes │ │ + ▼ │ +┌──────────────────────┐ │ +│ handleClientError/ │ │ +│ handleServerError │ │ +│ called (observe) │ │ +└──────────┬───────────┘ │ + │ │ + ▼ ▼ +┌─────────────────────────────────┐ +│ Original ClientError/ServerError│ +│ thrown │ +└─────────────────────────────────┘ +``` + +> **Important:** Handlers only **observe** errors; they do not transform or suppress them. The original error is always thrown. + +#### Usage Examples + +**Basic Usage: Logging** + +```swift +struct LoggingClientErrorHandler: ClientErrorHandler { + func handleClientError(_ error: ClientError) { + print("🚨 Client error in \(error.operationID): \(error.causeDescription)") + } +} + +let config = Configuration( + clientErrorHandler: LoggingClientErrorHandler() +) +``` + +**Telemetry Integration** + +```swift +struct AnalyticsClientErrorHandler: ClientErrorHandler { + let analytics: AnalyticsService + + func handleClientError(_ error: ClientError) { + analytics.track("client_error", metadata: [ + "operation": error.operationID, + "status": error.response?.status.code as Any, + "cause": error.causeDescription, + "timestamp": Date().ISO8601Format() + ]) + } +} +``` + +**Conditional Logging (Operation ID Based)** + +```swift +struct SelectiveLoggingHandler: ClientErrorHandler { + let criticalOperations: Set + + func handleClientError(_ error: ClientError) { + if criticalOperations.contains(error.operationID) { + // Send immediate alert for critical operations + alertService.sendAlert( + message: "Critical operation failed: \(error.operationID)", + severity: .high + ) + } + + // Log all errors + logger.error("[\(error.operationID)] \(error.causeDescription)") + } +} +``` + +**Server-Side Error Handler** + +```swift +struct ServerErrorLoggingHandler: ServerErrorHandler { + func handleServerError(_ error: ServerError) { + logger.error(""" + Server error: + - Operation: \(error.operationID) + - Request: \(error.request) + - Cause: \(error.underlyingError) + """) + } +} + +let config = Configuration( + serverErrorHandler: ServerErrorLoggingHandler() +) +``` + +### API stability + +This change maintains **full backward compatibility**: + +- The `clientErrorHandler` and `serverErrorHandler` parameters default to `nil`, so existing code works without modification. +- Existing `Configuration` initialization code continues to work unchanged. + +```swift +// Existing code - works without changes +let config = Configuration() +let config = Configuration(dateTranscoder: .iso8601) + +// Using new features +let config = Configuration( + clientErrorHandler: LoggingClientErrorHandler() +) +let config = Configuration( + clientErrorHandler: LoggingClientErrorHandler(), + serverErrorHandler: ServerErrorLoggingHandler() +) +``` + +### Test plan + +**Unit Tests** + +1. **Default Behavior Tests** + - Verify errors are thrown normally when `clientErrorHandler` is `nil` + - Verify errors are thrown normally when `serverErrorHandler` is `nil` + +2. **Handler Invocation Tests** + - Verify `handleClientError` is called when `ClientError` occurs + - Verify `handleServerError` is called when `ServerError` occurs + - Verify original error is thrown after handler invocation + +3. **Sendable Conformance Tests** + - Verify handler protocols properly conform to `Sendable` + +4. **Error Context Tests** + - Verify `ClientError`/`ServerError` passed to handlers contains correct context + +**Integration Tests** + +1. **Real API Call Scenarios** + - Verify handlers are called for various error situations including network errors and decoding errors + +2. **Performance Tests** + - Verify error handler addition has minimal performance impact + +### Future directions + +- **Async handler methods**: The current design uses synchronous handler methods. A future enhancement could introduce async variants for handlers that need to perform asynchronous operations like remote logging. + +- **Error transformation**: While this proposal focuses on error observation, a future proposal could introduce error transformation capabilities, allowing handlers to modify or replace errors before they are thrown. + +- **Built-in handler implementations**: The runtime could provide common handler implementations out of the box, such as a `LoggingErrorHandler` that integrates with swift-log. + +### Alternatives considered + +**Using middleware for error handling** + +One alternative considered was extending the existing `ClientMiddleware` and `ServerMiddleware` protocols to handle errors. However, middleware operates at the HTTP request/response level and cannot intercept errors that occur during response decoding or other runtime operations. The error handler approach provides a more comprehensive solution for error observation. + +**Closure-based handlers instead of protocols** + +Instead of defining `ClientErrorHandler` and `ServerErrorHandler` protocols, we could use closure properties directly: + +```swift +public var onClientError: ((ClientError) -> Void)? +``` + +While this approach is simpler, the protocol-based design was chosen because: +- It allows for more complex handler implementations with internal state +- It provides better documentation through protocol requirements +- It follows the existing patterns in swift-openapi-runtime (e.g., `DateTranscoder`, `MultipartBoundaryGenerator`)