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:
-
Session level. When generating your own Session, configure Interceptor: let Session = Session(Interceptor: SignRequestInterceptor()). This configuration applies to each Request created by the Session.
-
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:
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
SessionStateProvider
: To avoid direct useSession
Object, used hereSessionStateProvider
willSession
andSessionDelegate
Isolated.
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
-
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:
CompositeEventMonitor
A mixer for listeners that can be used to merge multiple listeners together.ClosureEventMonitor
Closure listeners, willEventMonitor
Each method is called back through closures.NSLoggingEventMonitor
Log listener, which outputs logs to the console.AlamofireNotifications
Notification listener, responsible for the corresponding event in the form of notification, here only part of the implementation of the listening method.
-
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:
SessionDelegate.urlSession(_:task:didCompleteWithError:)
System callback received.sessionDelegate
throughstateProvider
The callbackSession.didCompleteTask(_:completion:)
informSession
Mission accomplished. At this timeSession
It will decide whether to follow the specific statusrequestTaskMap
Delete the task from the record.sessionDelegate
The callbackRequest.didCompleteTask(_:with:)
. At this timeRequest
The response is validated before the next retry judgment phase.Request.retryOrFinish(error:)
If no errors occur, proceed directly to completion. Otherwise, go to the next retry.Request
Will 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 implementedSignRequestInterceptor
It 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:
open class Adapter: RequestInterceptor { ... }
: provides closure style request adapters.open class Retrier: RequestInterceptor { ... }
: provides closure style request retries.open class Interceptor: RequestInterceptor { ... }
: interceptor mixer, which can wrap multiple interceptors.public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator { ... }
: Provides the authorization function.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.