Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or
- <doc:SOAR-0012>
- <doc:SOAR-0013>
- <doc:SOAR-0014>
- <doc:SOAR-0015>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# SOAR-0015: Client Response Decoding Error Callback

Add a small client-side callback for observing response decoding errors.

## Overview

- Proposal: SOAR-0015
- Author(s): [mikolaj92](https://github.com/mikolaj92)
- Status: **Awaiting Review**
- Issue: [apple/swift-openapi-generator#912](https://github.com/apple/swift-openapi-generator/issues/912)
- Implementation:
- [mikolaj92/swift-openapi-runtime@error-handler-observability](https://github.com/mikolaj92/swift-openapi-runtime/tree/error-handler-observability)
- Affected components:
- runtime
- Related links:
- [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162)
- [apple/swift-openapi-generator#850](https://github.com/apple/swift-openapi-generator/pull/850)

### Introduction

This proposal adds an optional `clientResponseDecodingErrorHandler` callback to `Configuration`.

The callback receives the `ClientError` produced when a generated client call fails while decoding the response. It can log the error or report it to production monitoring. The same error continues to be thrown to the caller.

### Motivation

The problem is narrow: response decoding failures in production.

When a generated client call fails while decoding a response body, adopters often want to record that centrally. Today the direct way to do that is to wrap every call site:

```swift
do {
let output = try await client.getUser(.init(path: .init(id: id)))
// use output
} catch {
errorReporter.record(error)
throw error
}
```

That works, but it is easy to miss a call site. It also repeats the same reporting code across the app.

Middleware is useful for HTTP-level logging: it can observe the request and response around the transport call. This callback covers the later step where the runtime decodes the response body into the generated output type.

The runtime already wraps that failure in `ClientError`, which has the context needed for reporting: operation ID, input, request, response, response body, cause, and underlying error. The original decoding error remains available as `underlyingError`. The missing piece is one central callback for this decoding path.

### Proposed solution

Add this optional callback to `Configuration`:

```swift
public struct Configuration: Sendable {
public var clientResponseDecodingErrorHandler: (@Sendable (ClientError) -> Void)?

public init(
dateTranscoder: any DateTranscoder = .iso8601,
jsonEncodingOptions: JSONEncodingOptions = [.sortedKeys, .prettyPrinted],
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil,
clientResponseDecodingErrorHandler: (@Sendable (ClientError) -> Void)? = nil
)
}
```

The callback is called only when response decoding fails.

It receives `ClientError` rather than the raw decoding error because the wrapped error includes the operation and HTTP context. The raw decoding error remains available through `ClientError.underlyingError`.

Example:

```swift
let configuration = Configuration(
clientResponseDecodingErrorHandler: { error in
errorReporter.record(
error.underlyingError,
metadata: [
"operationID": error.operationID,
"status": error.response?.status.code.description ?? "none",
"cause": error.causeDescription
]
)
}
)

let client = Client(
serverURL: serverURL,
configuration: configuration,
transport: transport
)
```

Callers still catch the same `ClientError` they catch today.

### Relation to earlier work

This proposal builds on the work in [apple/swift-openapi-runtime#162](https://github.com/apple/swift-openapi-runtime/pull/162) and [apple/swift-openapi-generator#850](https://github.com/apple/swift-openapi-generator/pull/850).

Those PRs identified the right production problem and collected useful maintainer feedback. This proposal keeps the same proposal number because it is meant as a simpler version of that work, focused on the immediate need: observing response decoding failures in the upstream runtime.

### Detailed design

`UniversalClient.send` already has a response deserializer step:

```swift
deserializer: @Sendable (HTTPResponse, HTTPBody?) async throws -> OperationOutput
```

The implementation wraps errors thrown by that step into `ClientError`, calls `clientResponseDecodingErrorHandler` if it exists, then throws the same `ClientError`.

The callback is part of the response decoding step. This scope is intentional: the feature exists to observe the response decoding failure needed by adopters today.

### API stability

This is additive runtime public API.

Existing code keeps working because `clientResponseDecodingErrorHandler` defaults to `nil`. This is a new optional initializer parameter at the end of `Configuration.init`.

### Future directions

The same pattern could be considered for other parts of the client call in future proposals. This proposal keeps the first step focused on response decoding because that is the production problem we need to solve now.

### Alternatives considered

#### Protocol-based handler

The previous proposal used protocol-based handlers. That is a valid shape for a broader error handling API, but for this response decoding case a callback is simpler and easier to read. It also keeps the call site clear: adopters provide one closure that receives the wrapped `ClientError`.

This proposal starts with response decoding because that is the concrete production need. A wider observability API can be considered later if adopters need it.