# Advanced HTTP Client - [Advanced HTTP Client](#advanced-http-client) - [Why using a custom HTTPClient](#why-using-a-custom-httpclient) - [Validate Responses: Validators](#validate-responses-validators) - [Approve the response](#approve-the-response) - [Approve with custom response](#approve-with-custom-response) - [Fail with error](#fail-with-error) - [Retry with strategy](#retry-with-strategy) - [The Default Validator](#the-default-validator) - [Alt Request Validator](#alt-request-validator) - [Custom Validators](#custom-validators) - [Retry After [Another] Call](#retry-after-another-call) ## Why using a custom HTTPClient When your app is complex and you need to manage different http webservices, or you want to avoid using the shared client to create a decupled implementation, creating a custom `HTTPClient` is the best thing you can do. With a custom `HTTPClient` you can define your own rules to validate a response coming from a particular webservices, handling all the particular responses and edge cases you can encounter. This is a custom client: ```swift public lazy var b2cClient: HTTPClient = { var config = URLSessionConfiguration.default config.httpShouldSetCookies = true config.networkServiceType = .responsiveData let client = HTTPClient(baseURL: "https://myappb2c.ws.org/api/v2/",configuration: config) // Setup some common HTTP Headers for all requests client.headers = HTTPHeaders([ .init(name: .userAgent, value: myAgent), .init(name: "X-API-Experimental", value: "true") ]) return client }() ``` It contains a particular `URLSessionConfiguration` and a set of common HTTP Headers which are automatically used by any `HTTPRequest` you run in. ## Validate Responses: Validators Each raw response from a network call can be validated by a set of objects conform to the `HTTPValidator` object. `HTTPClient` instances has a `validators` array which may contains an ordered list of validators which respond to this method: ```swift func validate(response: HTTPResponse, forRequest request: HTTPRequest) -> HTTPResponseValidatorResult { } ``` This function analyze the response coming from request and decide the next step. Particularly you can return: ### Approve the response `nextValidator` The response is okay, you can move to the next validator (if any) or return the response received by the server. The `HTTPResponse` is a class where the method are open, so you can alter these values inside the validators if you need. ### Approve with custom response `nextValidatorWithResponse` The response is okay, you can move to the next validator (if any) or return. In this case you can return a new `HTTPResponse` subclass with additional properties. This is the case where you must do some additional business logic with your response before sending it outside the library. ### Fail with error `failChain(Error)` Received response is not valid (for example you have an `error` node in your json response which indicates the failure). You can parse the response and return a custom error bypassing the initial response. ### Retry with strategy `retry(HTTPRetryStrategy)` Something bad has occurred; you can however retry if `maxRetries` of the `HTTPRequest` is > 1. The options are: - `immediate` (will retry the original call immediately) - `delayed` (will retry the original call after a given amount of seconds), - `exponential` and `fibonacci` (same of the `delayed` with different time based upon the attempt made) - `after(HTTPRequest, TimeInterval, AltRequestCatcher?)` retry the original call after calling an alternate request. For example you are making an authenticated request and session has expired; you can therefore call a login alt request to perform a new login and retry the original call. We'll take a closer look at these strategies below. ## The Default Validator Each new client implement a single `validators` object called `HTTPDefaultValidator`. This object contains the standard logic to validate a response from server. Particularly it: - Check for empty responses. If you set `allowsEmptyResponses = false` when an empty response has received the chain fail with `HTTPError(.emptyResponse)` error. - Check the HTTP status code. If the code is an error code the chain may fail *(see the check above)* - If HTTP status code is an error or underlying `URLSession` received an error code (timeout, connection drop etc.) the `retriableHTTPStatusCodes` map is read. If the error is in that list a new retry may be triggered (only if `maxRetries` of the original `HTTPRequest` > 0). This validator should never be removed unless you have a really different logic to parse and validate errors. Typically you may want to add a new validator after this in order to perform your own logic based upon the uniqueness of your webservice. ## Alt Request Validator RealHTTP also provide a special validator called `HTTPAltRequestValidator`. This validator can be used when you need to execute a specific `HTTPRequest` if another request fails for certain reason. A typical example is the silent login operation; if you receive an `unathorized` or `.forbidden` error for a protected resource you may want to try a silent login operation, then re-execute initial failed request. The `HTTPAltRequestValidator` is triggered by certain HTTP status code; by default `401/403` and require a callback which return a specific `HTTPRequest` for a certain failed request. > **NOTE:** By default this validator is not triggered by network failure. If you want to perform it even when no response is received from server add the `HTTPStatusCode` `.none` to the list of `statusCodes` property. Usually you may want to be the first validator (before the default one). This is an example which perform a ```swift let client = HTTPClient(...) // The alt validator is triggered only when 401 error is received from any request's response. let authValidator = HTTPAltRequestValidator(statusCodes: [.unauthorized], { request, response in // If triggered here you'll specify the alt call to execute in order to refresh a JWT session token // before any retry of the initial failed request. return HTTPRequest("https://.../refreshToken") } onReceiveAltResponse: { request, response in // Once you have received response from your `refreshToken` call // you can do anything you need to use it. // In this example we'll set the global client's authorization header. let receivedToken = response.data... client.headers.set(.authorization, receivedToken) } // append at the top of the validators chain client.validators.insert(authValidator, at: 0) ``` ## Custom Validators When your client has a custom logic to return responses you can create your own validator to ensure all your requests are managed by a single validation code. Consider a webservices which always return a JSON object with the following keys: - `code`: `0` if everything is okay, `1` if an error has occurred - `errorMsg`: the message error, if not `null` something bad occurred - `data`: a dictionary with the response of the request, must be always present We can create a custom validator for this logic as seen below: ```swift import SwiftyJSON public class MyBadWSValidator: HTTPValidator { public func validate(response: HTTPResponse, forRequest request: HTTPRequest) -> HTTPResponseValidatorResult { // Structure logic check guard let data = response.data, let jsonData = JSON(data) else { return .failChain(HTTPError(.invalidResponse)) // response must be always JSON, no retry is allowed } guard data["code"].intValue == 0 else { let errorMsg = data["errorMsg"].string ?? "Unknown error" return .failChain(HTTPError(.internal, errorMsg)) // an error has occurred } // Business logic check let isRetriable = data["retriable"].boolValue guard data["data"].notExist == true, data["data"].type != JSON.Type.dictionary else { if isRetriable { return .retry(.fibonacci) } return .failChain(HTTPError(.invalidResponse)) // response must be always JSON, no retry is allowed } return .nextValidator // everything is okay } } ``` To add this validator to your client next up the default one just append it to the `validators` property: ```swift // Configure client let client = HTTPClient(...) client.validators.append(MyBadWSValidator()) // Prepare a request let req = HTTPRequest(...) req.maxRetries = 3 let result = try await req.fetch(client) ``` Once you set it all the requests executed in this client will be also validated by your own validator. ## Retry After [Another] Call A particular type of retry strategy is the one call `.after(HTTPRequest, TimeInterval, AltRequestCatcher?)`. It allows you to execute an alternate request if your initial fails, then retry the initial again. This kind of retry is particularly useful to make a silent login when an authenticated request fails due to expired sessions. Consider this auth call: ```swift let usersBooks = HTTPRequest("https://.../user/books/scifi") userBooks.headers = HTTPHeaders([ "X-Token": authToken ]) let books = try await usersBooks.fetch() ``` What happend if token is expired? Your call fails with a poor user experience. You can make a better experience attempting to refresh the token and retry automatically your call. How? First of all create your own custom validator: ```swift import SwiftyJSON public class SilentLoginValidator: HTTPValidator { /// This is the request which is used to refresh the token public var tokenRefreshRequest: HTTPRequest public func validate(response: HTTPResponse, forRequest request: HTTPRequest) -> HTTPResponseValidatorResult { guard response.statusCode == .unauthorized else { // If unauthorized error has occurred we'll try to make a silent login and // retry the initial request after 0.3 seconds by setting the authorization token, let silentLogin: HTTPRetryStrategy = .after(tokenRefreshRequest, 0.3) { request, response in if let response = JSONSerialization.jsonObject(with: response.data ?? Data(), options: .fragmentsAllowed) as? [String: String] { // Set the new received token request.headers.set("X-Token", response["token"] as! String) } } return .retry(silentLogin) } // No error, move to the next validator return .nextValidator } } ``` Just set this validator to automatically retry after doing a silent login.