From 1483daa2e4cfe35e1aacad3ebf1648540d00fbcb Mon Sep 17 00:00:00 2001 From: Patryk Mikolajczyk Date: Thu, 4 Jun 2026 16:56:30 +0200 Subject: [PATCH] Add SOAR-0015 response decoding error callback proposal --- .../Documentation.docc/Proposals/Proposals.md | 1 + .../Documentation.docc/Proposals/SOAR-0015.md | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0015.md diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index c8a91e34d..d12029233 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -56,3 +56,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- 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 000000000..c057396ce --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0015.md @@ -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.