Skip to content

[Proposal] Add Delegate Callback for Decoding Result #100

@winnisx7

Description

@winnisx7

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 /users finishes decoding into a User model. Right now every call site must wrap send() 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 callbackQueue as send() completion (background or main, chosen by the user).
  • Source compatibility – Provide a default empty implementation via an extension so 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

  1. Extension wrappers – Provide a sendAndLog() helper, but users must replace every send() call.
  2. 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's EventMonitor, OkHttp's EventListener.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 Result right after decoding in APIClient.send(_:) and invoke the delegate.

If this proposal looks reasonable, I'm happy to submit a PR. 😊

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions