Network Client — Interceptors, Validators & DI
Every iOS app communicates with some back end server to sync data.Swift itself has very well defined set of classes for this purpose — URLSession. There has been several other tools too available which are being used frequently and provide additional wrappers, like Alamofire.
In this story, I am mainly going to discuss- such a Network Client which can have any tool (URL Session, Alamofire etc) for API call and provides additional support for frequent challanges developers face in consumption of those and they have to write up extra code for that —
- Interceptors — Interceptors are a powerful mechanism that can pre-process or post-process a request with defined set of steps.
- Validators — Validating Request / Response at common place
- Dependency Injection
All these has been setup as Swift Library at GitHub. Refer RCNetworkClient
Interceptors -
In a particular project, we follow some defined rules for each Request / Response, like
- Auth Headers
- Device / User Info in each request
- Encryption of PayLoad
- Decryption of PayLoad
Interceptors are a powerful mechanism that can pre-process or post-process a request
We have declared two protocols — RequestInterceptor & ResponseInterceptor having one method declaration
Request Interceptors — Pre Request Execution Headers Definition — Common Headers Modify Request Body — Append Common details, viz user / device details, wrap body in defined structure, encrypt request body etc
Response Interceptors — Post Response Execution Parse Response Body — Parse Received response, viz decrypt response data, extract required elements etc — Validate Tokens — Retry/ Validate Session Token and use Event Bus
class CommonRequestHeaderInterceptors : RequestInterceptor {
func interceptRequest(request: inout URLRequest) {
request.addValue("Bearer " + (APIDataManager.appToken ?? ""), forHTTPHeaderField: "Authorization")
}
}
Validators
Similarly, for all API Calls, we need to validate request body & response body to make sure mandatory parameters are handled correctly. In general, Each API has two set of Responses — Success / Error Response, supporting both in a single API Call.
- Serialization / Deserialization
- Fallback Mechanism — Success / Error DAOs
- Common Error Generation
- Token Management
Network Layer is capable of handling and any request Encodable/ Decodable object serialization / deserialization. We have done that using Generics in POP. Request is hit only if serialization adhering to all mandatory parameters is fulfilled.
For each success response — there is always some error response. Library is capable enough to accept both success / error DAO type and parse response accordingly. Response is reverted only if success DAO deserialization adhering to all mandatory parameters is fulfilled. Otherwise fallback mechanism to deserialize data into error DAO is attempted. Even if that Deserialization is successful — error will be returned as error DAO in error block only
In case any other unexpected error happens. Neither Success nor Error DAO is deserialized then another Decodable error is prepared with respective error code and message and reverted in error block
Another protocol has been defined for unauthorized access — 401 — bearer token expiration handler — refresh token using that and save in your app data manager — and retry same request
We have declared one protocol — RetryInterceptor having one method declaration
class AppTokenRefreshWrapper : RetryInterceptor{
func retryRequest(onSuccess: @escaping () -> Void, onError: @escaping (APITimeError) -> Void) {
let params : String = "grant_type=client_credentials"
RefreshAppTokenAPI().execute(requestBody: params, onSuccessResponse: { (appToken) in
APIDataManager.appToken = appToken.access_token
onSuccess()
}, onErrorResponse: { (error) in
onError(APITimeError.init(errorCode: RCNetworkConstants.inValidResponse.rawValue, message: RCNetworkConstants.inValidResponse.rawValue))
}) { (error) in
onError(error)
}
}
}
Dependency Injection
For making API Call, one default client has already been added to this library — CoreNetworkClient — which uses URLSession.
In case, you want to use some other client — like Alamofire or FCM etc. you an define your own client and declare that in project level setup against var networkClient. It can be customised per API call as well using overridden approach as discussed above.
We have declared one protocol — NetworkDispatcher having one method declaration
class AlamofireNetworkClient : NetworkDispatcher{ func consumeRequest(request: URLRequest, onSuccess: @escaping (HTTPURLResponse, Data?) -> Void, onError: @escaping (APITimeError) -> Void) { Alamofire.request(request) .validate() .responseData { (response) in guard response.result.isSuccess, let httpResponse = response.response else { let customError = APITimeError.init(errorCode: "\(response.response?.statusCode ?? -1)",message: response.result.error?.localizedDescription ?? "unexpectedError", receivedResponse: response.data) onError(customError) return } guard let responseData = response.data else{ let customError = APITimeError.init(errorCode: "\(httpResponse.statusCode)", message: "unexpectedError", receivedResponse: response.data) onError(customError) return } onSuccess(httpResponse, responseData) } }}
Unit Test
We can have Mock Network Dispatcher for our Unit Tests target and define our own required responses as per logic test.
class CoreNetworkClientMock : NetworkDispatcher { private var targetResponse : Data? private var errorResponse : APITimeError? init(success:Data?, error:APITimeError?) { targetResponse = success errorResponse = error } func consumeRequest(request: URLRequest, onSuccess: @escaping (HTTPURLResponse, Data?) -> Void, onError: @escaping (APITimeError) -> Void) { if targetResponse != nil { let fakeResponse = HTTPURLResponse.init() onSuccess(fakeResponse, targetResponse) }else { onError(errorResponse!) } }}
Integration
At Project Level — Add Extension to APIRequest protocol to define default behaviours across API Calls.
public extension APIRequest { var retryInterceptor : (interceptor : RetryInterceptor, errorCodes : [String])?{ get{ (AppTokenRefreshWrapper(), ["401", "400"]) } } var interceptors : (requestInterceptors: [RequestInterceptor],responseInterceptors: [ResponseInterceptor])? { get { ([CommonRequestHeaderInterceptors()], []) } } var networkClient : NetworkDispatcher { get { CoreNetworkClient() } } var mimeType: String { get{ "" } } var timeoutInterval : TimeInterval { get{ 30 } } var cachingPolicy : URLRequest.CachePolicy { get{ URLRequest.CachePolicy.useProtocolCachePolicy } }
}
API Call Declaration
- API call can be declared using structs, its simple
- Implement Protocols for required Request Type viz GET, POST
struct HomeFeedAPI: GETAPIRequest { typealias ResponseType = HomeResponseDAO typealias ErrorResponseType = AppTokenErrorDAO var endPoint: String { return PlatformConstants.serverURL + PlatformConstants.homeFeed }}
For a particular API, if defaults are needed to be overridden then that can also be done as per below-
struct RefreshAppTokenAPI: POSTAPIRequest { typealias RequestBodyType = String typealias ResponseType = AppTokenDAO typealias ErrorResponseType = AppTokenErrorDAO var endPoint: String { return PlatformConstants.serverURL + PlatformConstants.refreshAppToken } var interceptors: (requestInterceptors: [RequestInterceptor], responseInterceptors: [ResponseInterceptor])? { return ([TokenRequestHeaderInterceptor()],[]) }}
Execution
HomeFeedAPI().execute(requestParams: queryParams, onSuccessResponse: { [weak self] (response) in // Received Success Response DAO }, onErrorResponse: { [weak self] (error) in // Received Error Response DAO }) { [weak self] (error) in // Received Generic Error }
For more details —
All these has been setup as Swift Library at GitHub. Refer RCNetworkClient