-
-
Notifications
You must be signed in to change notification settings - Fork 77
Description
Summary
APIClientDelegate currently exposes hooks for request preparation (willSendRequest), retry decisions (shouldClientRetry), and custom encoder/decoder injection. However, there is no way to observe the moment after validate(_:) passes and the response has been successfully decoded into its model type. This makes centralized logging, monitoring, and tracing cumbersome.
Example: When integrating Pulse, Sentry, or
os_log, we often need to record the exact time a request such as/usersfinishes decoding into aUsermodel. Right now every call site must wrapsend()manually.
Motivation
- Observability – Bridge the gap between network‑level events and model‑level data in one timeline.
- Centralized logging – Capture success/failure and timing information in a single delegate, without modifying every call site.
- Consistent reference point – Use the moment after
validate+ decoding as the authoritative "request completed" milestone.
Proposed API
public protocol APIClientDelegate: AnyObject {
/* existing methods … */
/// Called when `validate(_:)` has succeeded and the response body has been
/// decoded to its model type.
/// - Parameters:
/// - client: The calling `APIClient`.
/// - result: `.success(Response)` or `.failure(Error)` (includes decoding errors).
/// - request: The original `Request` for inspection (path, method, etc.).
func client<Response>(
_ client: APIClient,
didReceiveResult result: Result<Response, Error>,
for request: Request<Response>
) where Response: Decodable
}- Threading – Fire on the same
callbackQueueassend()completion (background or main, chosen by the user). - Source compatibility – Provide a default empty implementation via an
extensionso existing code remains unaffected.
Example Usage
final class DecodedLogger: APIClientDelegate {
private let logger = Logger(label: "API.Decoded")
func client<Response>(
_ client: APIClient,
didReceiveResult result: Result<Response, Error>,
for request: Request<Response>
) where Response: Decodable {
switch result {
case .success(let value):
logger.debug("✅ \(request.path) decoded\n\(value)")
case .failure(let error):
logger.error("❌ \(request.path) decode error: \(error)")
}
}
}
// Configure
let client = APIClient(baseURL: base) { cfg in
cfg.delegate = DecodedLogger()
}Benefits
- Zero call‑site changes – Continue using
client.send(); decoding results are logged automatically. - Better observability – Pulse UI / Xcode Console now show a seamless flow: request ▶︎ validate ▶︎ decode ▶︎ business logic.
- Extensibility – Easy to emit custom metrics, tracing spans, etc., based on success or failure.
Drawbacks
- Slight increase in delegate surface area, though mitigated by default implementation.
Alternatives Considered
- Extension wrappers – Provide a
sendAndLog()helper, but users must replace everysend()call. - Post‑processing with Combine/AsyncSequence – Possible, but duplicates native async/await flow.
Additional Context
- Comparable hooks exist in other libraries/frameworks: Alamofire's
EventMonitor.request(_:didParseResponse:), Moya'sEventMonitor, OkHttp'sEventListener.responseBodyEnd(). - This change is binary‑ and source‑compatible and would unlock richer integrations with Pulse, os_log, and distributed tracing systems.
Implementation should be straightforward: create a
Resultright after decoding inAPIClient.send(_:)and invoke the delegate.
If this proposal looks reasonable, I'm happy to submit a PR. 😊