Skip to content

Interceptors

Husnain Ali edited this page Sep 27, 2026 · 2 revisions

Interceptors

Endpoint
  -> request interceptors (in order)      adapt headers, sign, add tracing
  -> authentication
  -> transport (URLSession / pinning)
  -> status mapping
  -> response interceptors (in order)     inspect, retry, fail, or substitute
  -> decode
  -> caller
struct AppVersionInterceptor: RequestInterceptor {
    func adapt(_ request: URLRequest, for endpoint: AnyEndpoint) async throws -> URLRequest {
        var r = request
        r.setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version")
        return r
    }
}

struct TwoFactorInterceptor: ResponseInterceptor {
    func process(_ response: InterceptorResponse, for endpoint: AnyEndpoint) async throws -> InterceptOutcome {
        guard response.statusCode == 428 else { return .proceed }
        try await promptForOTP()
        return .retry           // .proceed / .retry / .fail(NetworkError) / .substitute(Data, HTTPURLResponse)
    }
}

config.requestInterceptors  = [AppVersionInterceptor()]
config.responseInterceptors = [TwoFactorInterceptor()]

TracingInterceptor is always installed first and adds X-Request-ID to every request.

Response interceptors also run for upload and download transfers. A .retry outcome is ignored there, because a transfer is never re-sent, but .fail and a substituted body still take effect.

Clone this wiki locally