# 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) - [Retry by modify the original request](#retry-by-modify-the-original-request) ## Why use 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 and handle a response coming from a particular webservice, as well as any edge cases you may 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 on it. ## Validate Responses: Validators Each raw response from a network call can be validated by a set of objects that conform to the `HTTPValidator` protocol. `HTTPClient` instances have a `validators` array which may contain an ordered list of validators which respond to this method: ```swift func validate(response: HTTPResponse, forRequest request: HTTPRequest) -> HTTPResponseValidatorResult { } ``` This function analyzes the response coming from request and determines the next step. In particular, 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 methods are open, so you can alter these values inside the validators if you need. ### Approve with a 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; however, you can 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` are the same as `delayed`, but with sequentially increasing delay times - `after(HTTPRequest, TimeInterval, AltRequestCatcher?)` will retry the original call after calling an alternate request. For example, if you are making an authenticated request and the session has expired; you can then call a login alternate request to perform a new login and retry the original call. - `afterTask(TimeInterval, RetryTask, RetryTaskErrorCatcher?)` performs an async task before retrying the original request. By using an async `Task`, you may perform work outside of the scope of RealHTTP and inject whatever you need into the original request. Note that, like with the existing retry with `HTTPRequest` strategy, any error triggered by the async `Task` is not propagated to the original request. However, a callback could be provided to at least "see" it. We'll take a closer look at these strategies below. ## The Default Validator Each new client implements a single `validators` object called `HTTPDefaultValidator`. This object contains the standard logic to validate a response from the server. In particular, it: - Checks for empty responses. If you set `allowsEmptyResponses = false` when an empty response is received, the chain will fail with an `HTTPError(.emptyResponse)` error. - Checks 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 code or the underlying `URLSession` received an error code (timeout, connection drop etc.) the `retriableHTTPStatusCodes` map is read. If the error is in that list, then a new retry may be triggered (but only if `maxRetries` of the original `HTTPRequest` > 0). This validator should never be removed unless you have a really unusual use case for parsing and validating errors. Typically, you will want to add a new validator after this, in order to perform your own logic for handling the unique setup of your webservice. ## Alt Request Validator RealHTTP also provides a special validator called `HTTPAltRequestValidator`. This validator can be used when you need to execute a specific `HTTPRequest` if another request fails for a given reason. A typical example would be the silent login operation; if you receive an `unauthorized` or `.forbidden` error for a protected resource you may want to try a silent login operation, then re-execute the initial (failed) request. The `HTTPAltRequestValidator` is triggered by certain HTTP status codes; by default `401/403` require a callback which returns 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 in the `statusCodes` property. Often, you will want to execute your validator first (before the default one). This is an example which performs a JWT session token refresh before retrying the initial request: ```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 custom logic to run before returning responses you can create your own validator to ensure all your requests are managed by the same validation logic. Consider a webservice which always returns 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 to 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 also be validated by your own validator. ## Retry After [Another] Call The retry strategy called `.after(HTTPRequest, TimeInterval, AltRequestCatcher?)` allows you to execute an alternate request if the initial one fails, and then retry the initial one again. This kind of retry is particularly useful for silent login when an authenticated request fails due to an expired session. 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 happens if token is expired? Your call fails with a poor user experience. You can make it a better experience by attempting to refresh the token and automatically retry your call. How? First, 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 performing a silent login. ## Retry by modifying the original request This strategy performs an async task before retrying the original request. With the `after()` strategy, you must provide another `HTTPRequest` to be performed before the retry. If the request Authorization should be handled by something other than an HTTP service, there is no way to interface with the Validator to create/refresh the authorization before the retry. However, by using `afterTask()`, you may perform work outside of the scope of RealHTTP and inject whatever the user wants into the original request. Note that, like with the existing retry with `HTTPRequest` strategy, any error triggered by the async `Task` is not propagated to the original request. However, a callback could be provided to at least "see" it. ```swift // Define an async function called before retry the original request. let patchRequestAndRetryTask: (HTTPRequest) async throws -> Void = { originalRequest in originalRequest.headers.set(.authBearerToken("abcdefg")) } // Create a custom client with a callback validator to show up the action let newClient = HTTPClient(baseURL: nil) newClient.validators = [ CallbackValidator { response, request in if request.currentRetry < 2 { return .retry(.afterTask(4, { originalRequest in // we can specify an async function which modify the original request // before retry it, after 4 seconds. try await patchRequestAndRetryTask(originalRequest) }, { error in retryTaskError = error })) } else { // just one retry, if it fails an error is triggered return .failChain(HTTPError(.tooManyRequests)) } } ] // Execute any request let aRequest = HTTPRequest { // configure your request } let aResponse = try await aRequest.fetch(newClient) ```