Skip to content

Commit 3fc2ca4

Browse files
committed
SOAR-0015: Error Handler Protocols for Client and Server
1 parent c4f3f1e commit 3fc2ca4

File tree

1 file changed

+377
-0
lines changed
  • Sources/swift-openapi-generator/Documentation.docc/Proposals

1 file changed

+377
-0
lines changed
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
# SOAR-0015: Error Handler Protocols for Client and Server
2+
3+
Introduce `ClientErrorHandler` and `ServerErrorHandler` protocols for centralized error observation on both client and server sides.
4+
5+
## Overview
6+
7+
- Proposal: SOAR-0015
8+
- Author(s): [winnisx7](https://github.com/winnisx7)
9+
- Status: **Proposed**
10+
- Issue: [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162)
11+
- Implementation:
12+
- [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162)
13+
- Affected components:
14+
- runtime
15+
16+
### Introduction
17+
18+
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`.
19+
20+
### Motivation
21+
22+
Currently, swift-openapi-runtime provides limited options for centralized error handling. Developers face the following challenges:
23+
24+
**Problem 1: Scattered Error Handling**
25+
26+
Errors must be handled individually at each API call site, leading to code duplication and inconsistent error handling.
27+
28+
```swift
29+
// Current approach: individual handling at each call site
30+
do {
31+
let response = try await client.getUser(...)
32+
} catch {
33+
// Repeated error handling logic
34+
logger.error("API error: \(error)")
35+
}
36+
37+
do {
38+
let response = try await client.getPosts(...)
39+
} catch {
40+
// Same error handling logic repeated
41+
logger.error("API error: \(error)")
42+
}
43+
```
44+
45+
**Problem 2: Middleware Limitations**
46+
47+
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**.
48+
49+
**Problem 3: Telemetry and Logging Complexity**
50+
51+
To collect telemetry or implement centralized logging for all errors, developers currently need to modify every API call site.
52+
53+
**Problem 4: Difficulty Utilizing Error Context**
54+
55+
`ClientError` and `ServerError` contain rich context information such as `operationID`, `request`, and `response`, but centralized analysis using this information is difficult.
56+
57+
### Proposed solution
58+
59+
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.
60+
61+
```swift
62+
// Custom error handler with logging
63+
struct LoggingClientErrorHandler: ClientErrorHandler {
64+
func handleClientError(_ error: ClientError) {
65+
logger.error("Client error in \(error.operationID): \(error.causeDescription)")
66+
analytics.track("client_error", metadata: [
67+
"operation": error.operationID,
68+
"status": error.response?.status.code
69+
])
70+
}
71+
}
72+
73+
let config = Configuration(
74+
clientErrorHandler: LoggingClientErrorHandler()
75+
)
76+
let client = UniversalClient(configuration: config, transport: transport)
77+
```
78+
79+
### Detailed design
80+
81+
#### New Protocol Definitions
82+
83+
```swift
84+
/// A protocol for handling client-side errors.
85+
///
86+
/// Implement this protocol to observe and react to errors that occur during
87+
/// client API calls. The handler is invoked after the error has been wrapped
88+
/// in a ``ClientError``.
89+
///
90+
/// Use this to add logging, monitoring, or analytics for client-side errors.
91+
public protocol ClientErrorHandler: Sendable {
92+
/// Handles a client error.
93+
///
94+
/// This method is called after an error has been wrapped in a ``ClientError``
95+
/// but before it is thrown to the caller.
96+
///
97+
/// - Parameter error: The client error that occurred, containing context such as
98+
/// the operation ID, request, response, and underlying cause.
99+
func handleClientError(_ error: ClientError)
100+
}
101+
102+
/// A protocol for handling server-side errors.
103+
///
104+
/// Implement this protocol to observe and react to errors that occur during
105+
/// server request handling. The handler is invoked after the error has been
106+
/// wrapped in a ``ServerError``.
107+
///
108+
/// Use this to add logging, monitoring, or analytics for server-side errors.
109+
public protocol ServerErrorHandler: Sendable {
110+
/// Handles a server error.
111+
///
112+
/// This method is called after an error has been wrapped in a ``ServerError``
113+
/// but before it is thrown to the caller.
114+
///
115+
/// - Parameter error: The server error that occurred, containing context such as
116+
/// the operation ID, request, and underlying cause.
117+
func handleServerError(_ error: ServerError)
118+
}
119+
```
120+
121+
#### Configuration Struct Changes
122+
123+
```swift
124+
public struct Configuration: Sendable {
125+
// ... existing properties ...
126+
127+
/// Custom XML coder for encoding and decoding xml bodies.
128+
public var xmlCoder: (any CustomCoder)?
129+
130+
/// The handler for client-side errors.
131+
///
132+
/// This handler is invoked after a client error has been wrapped in a ``ClientError``.
133+
/// Use this to add logging, monitoring, or analytics for client-side errors.
134+
/// If `nil`, errors are thrown without additional handling.
135+
public var clientErrorHandler: (any ClientErrorHandler)?
136+
137+
/// The handler for server-side errors.
138+
///
139+
/// This handler is invoked after a server error has been wrapped in a ``ServerError``.
140+
/// Use this to add logging, monitoring, or analytics for server-side errors.
141+
/// If `nil`, errors are thrown without additional handling.
142+
public var serverErrorHandler: (any ServerErrorHandler)?
143+
144+
/// Creates a new configuration with the specified values.
145+
///
146+
/// - Parameters:
147+
/// - dateTranscoder: The transcoder for date/time conversions.
148+
/// - multipartBoundaryGenerator: The generator for multipart boundaries.
149+
/// - xmlCoder: Custom XML coder for encoding and decoding xml bodies.
150+
/// - clientErrorHandler: Optional handler for observing client-side errors. Defaults to `nil`.
151+
/// - serverErrorHandler: Optional handler for observing server-side errors. Defaults to `nil`.
152+
public init(
153+
dateTranscoder: any DateTranscoder = .iso8601,
154+
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
155+
xmlCoder: (any CustomCoder)? = nil,
156+
clientErrorHandler: (any ClientErrorHandler)? = nil,
157+
serverErrorHandler: (any ServerErrorHandler)? = nil
158+
) {
159+
self.dateTranscoder = dateTranscoder
160+
self.multipartBoundaryGenerator = multipartBoundaryGenerator
161+
self.xmlCoder = xmlCoder
162+
self.clientErrorHandler = clientErrorHandler
163+
self.serverErrorHandler = serverErrorHandler
164+
}
165+
}
166+
```
167+
168+
#### UniversalClient Changes
169+
170+
In `UniversalClient`, the configured handler is called after the error has been wrapped in `ClientError`:
171+
172+
```swift
173+
// Inside UniversalClient (pseudocode)
174+
do {
175+
// API call logic
176+
} catch {
177+
let clientError = ClientError(
178+
operationID: operationID,
179+
request: request,
180+
response: response,
181+
underlyingError: error
182+
)
183+
184+
// Call handler if configured
185+
configuration.clientErrorHandler?.handleClientError(clientError)
186+
187+
throw clientError
188+
}
189+
```
190+
191+
#### UniversalServer Changes
192+
193+
Similarly, `UniversalServer` calls the handler after wrapping the error in `ServerError`.
194+
195+
#### Error Handling Flow
196+
197+
```
198+
┌──────────────────────┐
199+
│ API Call/Handle │
200+
└──────────┬───────────┘
201+
202+
▼ (error occurs)
203+
┌──────────────────────┐
204+
│ Wrap in ClientError/ │
205+
│ ServerError │
206+
└──────────┬───────────┘
207+
208+
209+
┌──────────────────────┐
210+
│ errorHandler │──────┐
211+
│ configured? │ │ No
212+
└──────────┬───────────┘ │
213+
Yes │ │
214+
▼ │
215+
┌──────────────────────┐ │
216+
│ handleClientError/ │ │
217+
│ handleServerError │ │
218+
│ called (observe) │ │
219+
└──────────┬───────────┘ │
220+
│ │
221+
▼ ▼
222+
┌─────────────────────────────────┐
223+
│ Original ClientError/ServerError│
224+
│ thrown │
225+
└─────────────────────────────────┘
226+
```
227+
228+
> **Important:** Handlers only **observe** errors; they do not transform or suppress them. The original error is always thrown.
229+
230+
#### Usage Examples
231+
232+
**Basic Usage: Logging**
233+
234+
```swift
235+
struct LoggingClientErrorHandler: ClientErrorHandler {
236+
func handleClientError(_ error: ClientError) {
237+
print("🚨 Client error in \(error.operationID): \(error.causeDescription)")
238+
}
239+
}
240+
241+
let config = Configuration(
242+
clientErrorHandler: LoggingClientErrorHandler()
243+
)
244+
```
245+
246+
**Telemetry Integration**
247+
248+
```swift
249+
struct AnalyticsClientErrorHandler: ClientErrorHandler {
250+
let analytics: AnalyticsService
251+
252+
func handleClientError(_ error: ClientError) {
253+
analytics.track("client_error", metadata: [
254+
"operation": error.operationID,
255+
"status": error.response?.status.code as Any,
256+
"cause": error.causeDescription,
257+
"timestamp": Date().ISO8601Format()
258+
])
259+
}
260+
}
261+
```
262+
263+
**Conditional Logging (Operation ID Based)**
264+
265+
```swift
266+
struct SelectiveLoggingHandler: ClientErrorHandler {
267+
let criticalOperations: Set<String>
268+
269+
func handleClientError(_ error: ClientError) {
270+
if criticalOperations.contains(error.operationID) {
271+
// Send immediate alert for critical operations
272+
alertService.sendAlert(
273+
message: "Critical operation failed: \(error.operationID)",
274+
severity: .high
275+
)
276+
}
277+
278+
// Log all errors
279+
logger.error("[\(error.operationID)] \(error.causeDescription)")
280+
}
281+
}
282+
```
283+
284+
**Server-Side Error Handler**
285+
286+
```swift
287+
struct ServerErrorLoggingHandler: ServerErrorHandler {
288+
func handleServerError(_ error: ServerError) {
289+
logger.error("""
290+
Server error:
291+
- Operation: \(error.operationID)
292+
- Request: \(error.request)
293+
- Cause: \(error.underlyingError)
294+
""")
295+
}
296+
}
297+
298+
let config = Configuration(
299+
serverErrorHandler: ServerErrorLoggingHandler()
300+
)
301+
```
302+
303+
### API stability
304+
305+
This change maintains **full backward compatibility**:
306+
307+
- The `clientErrorHandler` and `serverErrorHandler` parameters default to `nil`, so existing code works without modification.
308+
- Existing `Configuration` initialization code continues to work unchanged.
309+
310+
```swift
311+
// Existing code - works without changes
312+
let config = Configuration()
313+
let config = Configuration(dateTranscoder: .iso8601)
314+
315+
// Using new features
316+
let config = Configuration(
317+
clientErrorHandler: LoggingClientErrorHandler()
318+
)
319+
let config = Configuration(
320+
clientErrorHandler: LoggingClientErrorHandler(),
321+
serverErrorHandler: ServerErrorLoggingHandler()
322+
)
323+
```
324+
325+
### Test plan
326+
327+
**Unit Tests**
328+
329+
1. **Default Behavior Tests**
330+
- Verify errors are thrown normally when `clientErrorHandler` is `nil`
331+
- Verify errors are thrown normally when `serverErrorHandler` is `nil`
332+
333+
2. **Handler Invocation Tests**
334+
- Verify `handleClientError` is called when `ClientError` occurs
335+
- Verify `handleServerError` is called when `ServerError` occurs
336+
- Verify original error is thrown after handler invocation
337+
338+
3. **Sendable Conformance Tests**
339+
- Verify handler protocols properly conform to `Sendable`
340+
341+
4. **Error Context Tests**
342+
- Verify `ClientError`/`ServerError` passed to handlers contains correct context
343+
344+
**Integration Tests**
345+
346+
1. **Real API Call Scenarios**
347+
- Verify handlers are called for various error situations including network errors and decoding errors
348+
349+
2. **Performance Tests**
350+
- Verify error handler addition has minimal performance impact
351+
352+
### Future directions
353+
354+
- **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.
355+
356+
- **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.
357+
358+
- **Built-in handler implementations**: The runtime could provide common handler implementations out of the box, such as a `LoggingErrorHandler` that integrates with swift-log.
359+
360+
### Alternatives considered
361+
362+
**Using middleware for error handling**
363+
364+
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.
365+
366+
**Closure-based handlers instead of protocols**
367+
368+
Instead of defining `ClientErrorHandler` and `ServerErrorHandler` protocols, we could use closure properties directly:
369+
370+
```swift
371+
public var onClientError: ((ClientError) -> Void)?
372+
```
373+
374+
While this approach is simpler, the protocol-based design was chosen because:
375+
- It allows for more complex handler implementations with internal state
376+
- It provides better documentation through protocol requirements
377+
- It follows the existing patterns in swift-openapi-runtime (e.g., `DateTranscoder`, `MultipartBoundaryGenerator`)

0 commit comments

Comments
 (0)