In the previous post, we combed through the workflow of Alamofire. Today we’ll continue our research, this time focusing on RequestInterceptor.

RequestInterceptor is a protocol that has no requirements of its own, but follows two protocols:

public protocol RequestInterceptor: RequestAdapter.RequestRetrier {}
Copy the code

That is, to implement an interceptor, you need to satisfy a RequestAdapter and a RequestRetrier. Let’s look at what each of them wants.

RequestAdapter

The RequestAdapter is a RequestAdapter. For a request, we can use Adapter to determine how to operate the request. Specific definitions are as follows:

public protocol RequestAdapter {
    /// Decide how to handle the request using the urlRequest and session inputs. The result of processing is called back through Completion. Such as:
    1. We modify the request and then call back completion(.success(someRequest)). This allows the framework to continue processing the returned Request
    Call completion(.failure(someError)) back to completion(.failure(someError)).
    /// Since the callback is done via closure, asynchronous operations can also be done here
    func adapt(_ urlRequest: URLRequest.for session: Session.completion: @escaping (Result<URLRequest.Error- > >)Void)

    RequestAdapterState (RequestAdapterState is a structure containing session and requestID);
    func adapt(_ urlRequest: URLRequest.using state: RequestAdapterState.completion: @escaping (Result<URLRequest.Error- > >)Void)
}
Copy the code

If we split an HTTP request transaction into two phases: before the request is sent and after the response is received, the RequestAdapter here is used for the first phase. Does the RequestRetrier work on the second phase? Let’s look down.

RequestRetrier

public protocol RequestRetrier {
    /// This method is used to determine whether the request needs to be retried after an error. There are four ways we can handle this (from the RetryResult enumeration)
    func retry(_ request: Request.for session: Session.dueTo error: Error.completion: @escaping (RetryResult) - >Void)
}
Copy the code

You can see that this does work after the response is received, but it is limited to failure scenarios.

There are four possible ways to handle this:

public enum RetryResult {
    /// Retry immediately
    case retry
    /// Retry after the specified time
    case retryWithDelay(TimeInterval)
    /// No need to retry
    case doNotRetry
    /// No need to retry and report an error
    case doNotRetryWithError(Error)}Copy the code

All I have to do is call back the specific processing through completion callbacks to achieve the desired effect.

Okay, so much for the static part, let’s take a look at how RequestInterceptor works.

How does RequestInterceptor work

To see how the RequestInterceptor works, I create a new SignRequestInterceptor that completes the request signing, passing the signature through the request header:

Unit testing for RequestInterceptor is a bit complicated and a burden to understand, so we’ll use our own examples here.

class SignRequestInterceptor: RequestInterceptor {
    // MARK: - RequestAdapter
    
    func adapt(_ urlRequest: URLRequest.using state: RequestAdapterState.completion: @escaping (Result<URLRequest.Error- > >)Void) {
        let request = sign(request: urlRequest)
        completion(.success(request))
    }
    func adapt(_ urlRequest: URLRequest.for session: Session.completion: @escaping (Result<URLRequest.Error- > >)Void) {
        let request = sign(request: urlRequest)
        completion(.success(request))
    }
    // MARK: - RequestRetrier
    
    func retry(_ request: Request.for session: Session.dueTo error: Error.completion: @escaping (RetryResult) - >Void) {
        completion(.retry)
    }
    // MARK: -
    
    /// simulate the signature request, using the URL as the signature content for easy observation
    private func sign(request: URLRequest) -> URLRequest {
        guard let urlString = request.url?.absoluteString else {
            return request
        }
        var retRequest = request
        retRequest.headers.add(name: "X-SIGN", value: urlString)
        return retRequest
    }
}
Copy the code

Once we have our own RequestInterceptor, we have two ways to use it:

  1. Session level. When generating your own Session, configure Interceptor: let Session = Session(Interceptor: SignRequestInterceptor()). This configuration applies to each Request created by the Session.

  2. Request (“https://httpbin.org/post”, interceptor: SignRequestInterceptor())). This configuration applies only to the current Request.

RequestAdapter workflow

We put a break point here:

func sign(request: URLRequest) -> URLRequest { . }
Copy the code

After the request is initiated, you can see the following call stack:

Here is the final request configuration phase we discussed in the previous article, where our Interceptor is called. How to deal with it is up to your imagination.

The RequestRetrier workflow

Again, we put a break point on the following method:

func retry(_ request: Request.for session: Session.dueTo error: Error.completion: @escaping (RetryResult) - >Void) { . }
Copy the code

The network is then disconnected to simulate a network error. The corresponding call stack is as follows:

Calls can be traced back to the starting point is the system of the callback methods: SessionDelegate. UrlSession (_ : task: didCompleteWithError:). Before we look at the implementation, let’s take a look at some of the new faces introduced here:

  1. SessionDelegate: Achieved a lotURLSessionDelegate, interface system framework andAlamofire. It contains several important properties:
open class SessionDelegate: NSObject {
    // the file manager is responsible for downloading requested files
    private let fileManager: FileManager
    /// dependency, on which most operations depend, see SessionStateProvider
    weak var stateProvider: SessionStateProvider?
    /// event listener, responsible for notifying various events
    var eventMonitor: EventMonitor?
}
Copy the code
  1. SessionStateProvider: To avoid direct useSessionObject, used hereSessionStateProviderwillSessionandSessionDelegateIsolated.
protocol SessionStateProvider: AnyObject {
    /// HTTPS certificate validator
    var serverTrustManager: ServerTrustManager? { get }
    // redirect the handler
    var redirectHandler: RedirectHandler? { get }
    /// cache handler
    var cachedResponseHandler: CachedResponseHandler? { get }
    /// Task to request mapping
    func request(for task: URLSessionTask) -> Request?
    /// Report of statistics
    func didGatherMetricsForTask(_ task: URLSessionTask)
    /// Task completion report
    func didCompleteTask(_ task: URLSessionTask.completion: @escaping() - >Void)
    // task-level certificate mapping
    func credential(for task: URLSessionTask.in protectionSpace: URLProtectionSpace) -> URLCredential?
    /// Notify the request to cancel
    func cancelRequestsForSessionInvalidation(with error: Error?).
}
Copy the code

Here is the Session implementation of this protocol:

extension Session: SessionStateProvider {
    /// The task obtains the request directly from the requestTaskMap dictionary structure
    func request(for task: URLSessionTask) -> Request? {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        return requestTaskMap[task]
    }
    /// After the task completes, call back directly to Completion after determining that statistics have been collected. Otherwise use waitingCompletions for collection
    func didCompleteTask(_ task: URLSessionTask.completion: @escaping() - >Void) {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        // Returns true only if statistics have been collected
        let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterCompletingTask(task)
        if didDisassociate {
            completion()
        } else {
            waitingCompletions[task] = completion
        }
    }
    /// Call the callback of the waitingCompletions record when statistics are collected and the task is judged to be complete
    func didGatherMetricsForTask(_ task: URLSessionTask) {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        // True is returned only after the task has completed
        let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterGatheringMetricsForTask(task)
        if didDisassociate {
            waitingCompletions[task]?()
            waitingCompletions[task] = nil}}/// Obtain Request authentication information
    func credential(for task: URLSessionTask.in protectionSpace: URLProtectionSpace) -> URLCredential? {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        return requestTaskMap[task]?.credential ??
            session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace)
    }
    // invalidates all requests when the Session expires
    func cancelRequestsForSessionInvalidation(with error: Error?). {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        requestTaskMap.requests.forEach { $0.finish(error: AFError.sessionInvalidated(error: error)) }
    }
}
Copy the code
  1. EventMonitor: Event listener. This is also a protocol that can be used as an event listener to listen for a series of URLSession proxy events and various events during the Request lifecycle. All listener events have a default implementation, in the corresponding extension. Alamofire also provides multiple implementations:

    • CompositeEventMonitorA mixer for listeners that can be used to merge multiple listeners together.
    • ClosureEventMonitorClosure listeners, willEventMonitorEach method is called back through closures.
    • NSLoggingEventMonitorLog listener, which outputs logs to the console.
    • AlamofireNotificationsNotification listener, responsible for the corresponding event in the form of notification, here only part of the implementation of the listening method.
  2. RequestDelegate: Similar to SessionStateProvider, a Request communicates with a Session over this protocol

public protocol RequestDelegate: AnyObject {
    // get the Session configuration to generate the cURL command
    var sessionConfiguration: URLSessionConfiguration { get }
    /// Whether the request should be initiated immediately. True by default. The request. ResponseXXX parameter is used to determine whether to resume the request
    var startImmediately: Bool { get }
    /// Perform cleanup operations. For example, remove the downloaded file after the download is complete
    func cleanup(after request: Request)
    /// Request error, request for error handling
    func retryResult(for request: Request.dueTo error: AFError.completion: @escaping (RetryResult) - >Void)
    // retries are triggered for faulty requests
    func retryRequest(_ request: Request.withDelay timeDelay: TimeInterval?).
}
Copy the code

Here is the Session implementation of this protocol:

extension Session: RequestDelegate {
    // return the configuration of session (URLSession)
    public var sessionConfiguration: URLSessionConfiguration {
        session.configuration
    }
    // return the session property startRequestsImmediately
    public var startImmediately: Bool { startRequestsImmediately }
    /// Delete the Request from the active Request record when cleaning up
    public func cleanup(after request: Request) {
        activeRequests.remove(request)
    }
    
    /// Decide how to handle the request that has failed
    /// 1. Failed to obtain the request retry: the callback will not be retried
    // 2. The request retries are obtained. The request retries are processed according to the result:
    /// a: The retries returned an error: AFError after a callback wrapper
    /// b: Other: direct callback
    public func retryResult(for request: Request.dueTo error: AFError.completion: @escaping (RetryResult) - >Void) {
        guard let retrier = retrier(for: request) else {
            rootQueue.async { completion(.doNotRetry) }
            return
        }
        // Our retries will be called here
        retrier.retry(request, for: self, dueTo: error) { retryResult in
            self.rootQueue.async {
                guard let retryResultError = retryResult.error else { completion(retryResult); return }

                let retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error)
                completion(.doNotRetryWithError(retryError))
            }
        }
    }
    /// retry a request.
    public func retryRequest(_ request: Request.withDelay timeDelay: TimeInterval?). {
        rootQueue.async {
            let retry: () -> Void = {
                // The cancelled request will not be retried
                guard !request.isCancelled else { return }
                // Preparation: Record the retry times and reset the progress
                request.prepareForRetry()
                // The configuration phase of the request
                self.perform(request)
            }
            // If there is delay, execute by GCD; Otherwise, retry is triggered
            if let retryDelay = timeDelay {
                self.rootQueue.after(retryDelay) { retry() }
            } else {
                retry()
            }
        }
    }
}
Copy the code

The rest of the job is easy. The RequestRetrier process is simply a use of the above various methods:

  1. SessionDelegate.urlSession(_:task:didCompleteWithError:)System callback received.
  2. sessionDelegatethroughstateProviderThe callbackSession.didCompleteTask(_:completion:)informSessionMission accomplished. At this timeSessionIt will decide whether to follow the specific statusrequestTaskMapDelete the task from the record.
  3. sessionDelegateThe callbackRequest.didCompleteTask(_:with:). At this timeRequestThe response is validated before the next retry judgment phase.
  4. Request.retryOrFinish(error:)If no errors occur, proceed directly to completion. Otherwise, go to the next retry.
  5. RequestWill be calleddelegate(Session).retryResult(for:dueTo:completion:)Gets the result of whether there is a retry, if a retry is requireddelegate(Session).retryRequest(_:withDelay:)Retry. What we implementedSignRequestInterceptorIt was also inSession.retryResult(for:dueTo:completion:)Method to get the chance to be called.

That’s the general process, so you can get a general impression of each participant, and then follow the process. The overall picture is fairly clear.

Alamofire RequestInterceptor(s)

Some common interceptors are also implemented inside the framework, as follows:

  1. open class Adapter: RequestInterceptor { ... }: provides closure style request adapters.
  2. open class Retrier: RequestInterceptor { ... }: provides closure style request retries.
  3. open class Interceptor: RequestInterceptor { ... }: interceptor mixer, which can wrap multiple interceptors.
  4. public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator { ... }: Provides the authorization function.
  5. open class RetryPolicy: RequestInterceptor { ... }: Provides more control over retry conditions, such as the number of retries allowed, the request method allowed for retries, the interval between retries after each retry, and so on.

AuthenticationInterceptor and RetryPolicy is not too strong! 💯, there will be a special article analyzing them below, and keep an eye on expectations. 🤣

conclusion

Today we’ll focus on the workflow of the interceptor. As you can see, it gives us the opportunity to respond before and after a request transaction, but it’s not complicated.

In addition, I have put the corresponding code analysis on GitHub (branch: Risk), hoping to help you.