diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index 70e57332d1..9571a26575 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -46,7 +46,8 @@ module.exports = { 'fragments', 'caching', 'subscriptions', - 'swift-scripting' + 'swift-scripting', + 'request-pipeline', ] } } diff --git a/docs/source/initialization.md b/docs/source/initialization.md deleted file mode 100644 index b1ccf8e3f6..0000000000 --- a/docs/source/initialization.md +++ /dev/null @@ -1,344 +0,0 @@ ---- -title: Creating a client ---- - -Before you can execute GraphQL operations in your app, you need to initialize an `ApolloClient` instance. - -## Basic client creation - -In most cases, you can create a single shared instance of `ApolloClient` and point it at your GraphQL server. The recommended way to do this is to create a singleton: - -```swift -import Foundation -import Apollo - -class Network { - static let shared = Network() - - private(set) lazy var apollo = ApolloClient(url: URL(string: "http://localhost:8080/graphql")!) -} -``` - -Under the hood, this creates a client using `RequestChainNetworkTransport` with a default configuration. You can then use this client from anywhere in your code with `Network.shared.apollo`. - -## Advanced client creation - -For more advanced usage of the client, you can use this initializer which allows you to pass in an object conforming to the `NetworkTransport` protocol, as well as a store: - -```swift -public init(networkTransport: NetworkTransport, - store: ApolloStore) -``` - -The available implementations are: - -- **`RequestChainNetworkTransport`**, which passes a request through a chain of interceptors that can do work both before and after going to the network, and uses standard HTTP requests to communicate with the server -- **`WebSocketTransport`**, which will send everything using a web socket. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. -- **`SplitNetworkTransport`**, which will send subscription operations via a web socket and all other operations via HTTP. If you're using CocoaPods, make sure to install the `Apollo/WebSocket` sub-spec to access this. - -### Using `RequestChainNetworkTransport` - -The initializer for `RequestChainNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses: - -- `interceptorProvider`: The interceptor provider to use when constructing chains for a request. See below for details on interceptor providers. -- `endpointURL`: The GraphQL endpoint URL to use for all calls. -- `additionalHeaders`: Any additional headers that should be automatically added to **every** request, such as an API key or a language setting. Headers that should not be sent with every request (or whose values can change across requests) should be configured through an interceptor. Defaults to an empty dictionary. -- `autoPersistQueries`: Pass `true` if [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) should be used to send an operation's hash instead of the full operation body by default. **NOTE:** To use APQs, you need to make sure to generate your types with operation identifiers. In your Swift Script, make sure to pass a non-nil `operationIDsURL` to have this output. Due to this restriction, this option defaults to `false`. You will also want to make sure you're using the `AutomaticPersistedQueryInterceptor` in your chain after a network request has come back to handle known APQ errors. -- `requestCreator`: The `RequestCreator` object to use to build your `URLRequest`. Defaults to the provided `ApolloRequestCreator` implementation. -- `useGETForQueries`: Sends all requests of `query` type using `GET` instead of `POST`. This is mostly useful for large companies taking advantage of CDNs (Content Distribution Networks) that allow local caches instead of going all the way to your server for data which does not change often. This defaults to `false` to preserve existing behavior in older versions of the client. -- `useGETForPersistedQueryRetry`: Pass `true` to use `GET` instead of `POST` for a retry of a persisted query. Defaults to `false`. - -### How the `RequestChain` works - -A `RequestChain` is constructed using an array of interceptors, to be run in the order given, and handles calling back on a specified `DispatchQueue` after all work is complete. - -A chain is started by calling `kickoff`. This causes the chain to start running through the chain of interceptors in order. - -In each interceptor, work can be performed asynchronously on any thread. To move along to the next interceptor in the chain, call `proceedAsync`. - -By default, when the interceptor chain ends, if you have a parsed result available, this result will be returned to the caller. - -If you want to directly return a value to the caller, call `returnValueAsync`. If you want to have the chain return an error, call `handleErrorAsync`. Both of these methods will call your completion block on the queue specified when creating the `RequestChain`. - -Note that calling `returnValue` does **NOT** forbid calling `handleError` - or calling each more than once. For example, if you want to return data from the cache to the UI while a network fetch executes, you'd want to make sure that `returnValueAsync` was called twice. - -The chain also includes a `retry` mechanism, which will go all the way back to the first interceptor in the chain, then start running through the interceptors again. - -**IMPORTANT**: Do not call `retry` blindly. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying (especially if you're not using something like the `MaxRetryInterceptor` to limit how many retries are made). This **will** kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem! - -In the `RequestChainNetworkTransport`, each request creates an individual request chain, and uses an `InterceptorProvider` to figure out which interceptors should be handed to that chain. - -### Setting up `ApolloInterceptor` chains with `InterceptorProvider` - -Every operation sent through a `RequestChainNetworkTransport` will be passed into an `InterceptorProvider` before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation. - -Interceptors themselves are designed to be **short-lived**. A new set of interceptors should be provided for each request in order to avoid having multiple calls hitting the same instance of a single interceptor at the same time. - -Holding references to individual interceptors (outside of test verification) is generally not recommended. Instead, you can create an interceptor that holds on to a longer-lived object, and the provider can pass this object into each new set of interceptors. That way an interceptor itself can be easily disposable, but you don't have to recreate the underlying object doing heavier work. - -`DefaultInterceptorProvider` is a default implementation of an interceptor provider. It works with our existing parsing and caching system and tries to replicate the experience of using the old `HTTPNetworkTransport` as closely as possible. It takes a `URLSessionClient` and an `ApolloStore` to pass into the interceptors it uses. **This is the provider that developers will want to use at this time.** You can also sublcass this interceptor provider if you only need to insert interceptors at the beginning or end of the chain rather than intersperse them throughout. - -If you wish to make your own `InterceptorProvider` instead of using the provided one, you can take advantage of several interceptors that are included in the library: - -#### Pre-network -- `MaxRetryInterceptor` checks to make sure a query has not been tried more than a maximum number of times. -- `CacheReadInterceptor` reads from a provided `ApolloStore` based on the `cachePolicy`, and will return a result if one is found. - -#### Network -- `NetworkFetchInterceptor` takes a `URLSessionClient` and uses it to send the prepared `HTTPRequest` (or subclass thereof) to the server. - -#### Post-Network - -- `ResponseCodeInterceptor` checks to make sure a valid response status code has been returned. **NOTE**: Most errors at the GraphQL level are returned with a `200` status code and information in the `errors` array per the GraphQL Spec. This interceptor helps with things like server errors and errors that are returned by middleware. [This article on error handling in GraphQL](https://medium.com/@sachee/200-ok-error-handling-in-graphql-7ec869aec9bc) is a really helpful look at how and why these differences occur. -- `AutomaticPersistedQueryInterceptor` handles checking responses to see if an error is because an automatic persisted query failed, and the full operation needs to be resent to the server. -- `JSONResponseParsingInterceptor` parses code generated by our current Typescript-based code generation. -- `CacheWriteInterceptor` writes to a provided `ApolloStore` based on code from our current Typescript-based code generation. - -#### The Additional Error Interceptor - -The `InterceptorProvider` can optionally provide an `additionalErrorInterceptor` which will get called before returning an error to the caller, regardless of the origin of the error. This is mostly useful for logging and tracing errors. - -Note that if there is a particular _expected_ error, such as an expired authentication token, that type of error is best handled by having an interceptor within the interceptor chain, which will allow you to retry your request much more easily. - -### The URLSessionClient class - -Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. - -One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. - -By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `DefaultInterceptorProvider` use the default initializer for `URLSessionClient`. - -The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios. - -### Example Advanced Client Setup - -Here's a sample how to use an advanced client with some custom interceptors. This code assumes you've got the following classes in your own code (**these are not part of the Apollo library**): - -- **`UserManager`** to check whether the user is logged in, perform associated checks on errors and responses to see if they need to renew their token, and perform that renewal -- **`Logger`** to handle printing logs based on their level, and which supports `.debug`, `.error`, or `.always` log levels. - -#### Example interceptors - -##### Sample `UserManagementInterceptor` - -An interceptor which checks if the user is logged in and then renews the user's token if it is expired asynchronously before continuing the chain, using the above-mentioned `UserManager` class: - -```swift -import Apollo - -class UserManagementInterceptor: ApolloInterceptor { - - enum UserError: Error { - case noUserLoggedIn - } - - /// Helper function to add the token then move on to the next step - private func addTokenAndProceed( - _ token: Token, - to request: HTTPRequest, - chain: RequestChain, - response: HTTPResponse?, - completion: @escaping (Result, Error>) -> Void) { - - request.addHeader(name: "Authorization", value: "Bearer \(token.value)") - chain.proceedAsync(request: request, - response: response, - completion: completion) - } - - func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse?, - completion: @escaping (Result, Error>) -> Void) { - - guard let token = UserManager.shared.token else { - // In this instance, no user is logged in, so we want to call - // the error handler, then return to prevent further work - chain.handleErrorAsync(UserError.noUserLoggedIn, - request: request, - response: response, - completion: completion) - return - } - - // If we've gotten here, there is a token! - if token.isExpired { - // Call an async method to renew the token - UserManager.shared.renewToken { [weak self] tokenRenewResult in - guard let self = self else { - return - } - - switch tokenRenewResult { - case .failure(let error): - // Pass the token renewal error up the chain, and do - // not proceed further. Note that you could also wrap this in a - // `UserError` if you want. - chain.handleErrorAsync(error, - request: request, - response: response, - completion: completion) - case .success(let token): - // Renewing worked! Add the token and move on - self.addTokenAndProceed(token, - to: request, - chain: chain, - response: response, - completion: completion) - } - } - } else { - // We don't need to wait for renewal, add token and move on - self.addTokenAndProceed(token, - to: request, - chain: chain, - response: response, - completion: completion) - } - } -} -``` - -##### Sample `RequestLoggingInterceptor` - -An interceptor which logs the outgoing request using the above-mentioned `Logger` class, then moves on: - -```swift -import Apollo - -class RequestLoggingInterceptor: ApolloInterceptor { - - func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse?, - completion: @escaping (Result, Error>) -> Void) { - - Logger.log(.debug, "Outgoing request: \(request)") - chain.proceedAsync(request: request, - response: response, - completion: completion) - } -} -``` - -##### Sample `‌ResponseLoggingInterceptor` - -An interceptor using the above-mentioned `Logger` which logs the incoming response if it exists, and moves on. - -Note that this is an example of an interceptor which can both proceed **and** throw an error - we don't necessarily want to stop processing if this was set up in the wrong place, but we do want to know about it. - -```swift -import Apollo - -class ResponseLoggingInterceptor: ApolloInterceptor { - - enum ResponseLoggingError: Error { - case notYetReceived - } - - func interceptAsync( - chain: RequestChain, - request: HTTPRequest, - response: HTTPResponse?, - completion: @escaping (Result, Error>) -> Void) { - - defer { - // Even if we can't log, we still want to keep going. - chain.proceedAsync(request: request, - response: response, - completion: completion) - } - - guard let receivedResponse = response else { - chain.handleErrorAsync(ResponseLoggingError.notYetReceived, - request: request, - response: response, - completion: completion) - return - } - - Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)") - - if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) { - Logger.log(.debug, "Data: \(stringData)") - } else { - Logger.log(.error, "Could not convert data to string!") - } - } -} -``` - -#### Example Custom Interceptor Provider - -This `InterceptorProvider` uses all of the interceptors that (as of this writing) are in the `DefaultInterceptorProvider`, interspersed at the appropriate points with the sample interceptors created above: - -```swift -import Foundation -import Apollo - -struct NetworkInterceptorProvider: InterceptorProvider { - - // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they - // will be handed to different interceptors. - private let store: ApolloStore - private let client: URLSessionClient - - init(store: ApolloStore, - client: URLSessionClient) { - self.store = store - self.client = client - } - - func interceptors(for operation: Operation) -> [ApolloInterceptor] { - return [ - MaxRetryInterceptor(), - CacheReadInterceptor(store: self.store), - UserManagementInterceptor(), - RequestLoggingInterceptor(), - NetworkFetchInterceptor(client: self.client), - ResponseLoggingInterceptor(), - ResponseCodeInterceptor(), - JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), - AutomaticPersistedQueryInterceptor(), - CacheWriteInterceptor(store: self.store) - ] - } -} -``` - -#### Example Network Singleton Setup - -This is the equivalent of what you'd set up in the [Basic Client Creation](#basic-client-creation) section, and what you'd call into from your application. - -```swift -import Foundation -import Apollo - -class Network { - static let shared = Network() - - private(set) lazy var apollo: ApolloClient = { - // The cache is necessary to set up the store, which we're going to hand to the provider - let cache = InMemoryNormalizedCache() - let store = ApolloStore(cache: cache) - - let client = URLSessionClient() - let provider = NetworkInterceptorProvider(store: store, client: client) - let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")! - - let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, - endpointURL: url) - - - // Remember to give the store you already created to the client so it - // doesn't create one on its own - return ApolloClient(networkTransport: requestChainTransport, - store: store) - }() -} -``` - - -An example of setting up a client which can handle web sockets and subscriptions is included in the [subscription documentation](subscriptions/#sample-subscription-supporting-initializer). diff --git a/docs/source/initialization.mdx b/docs/source/initialization.mdx new file mode 100644 index 0000000000..b6d35d7ff6 --- /dev/null +++ b/docs/source/initialization.mdx @@ -0,0 +1,44 @@ +--- +title: Creating a client +--- + +Before you can execute GraphQL operations in your app, you need to initialize an `ApolloClient` instance. + +## Basic client creation + +In most cases, you can create a single shared instance of `ApolloClient` and point it at your GraphQL server. The recommended way to do this is to create a singleton like so: + +```swift +import Foundation +import Apollo + +class Network { + static let shared = Network() + + private(set) lazy var apollo = ApolloClient(url: URL(string: "http://localhost:4000/graphql")!) +} +``` + +Under the hood, this creates a client using the default network transport (`RequestChainNetworkTransport`) and default configuration. You can then use this client from anywhere in your code with `Network.shared.apollo`. + +> You should use this initializer unless you need to customize your client's network communication, such as to enable subscription operations. + +## Advanced client creation + +For advanced use cases, you can use a different `ApolloClient` initializer that enables you to customize your client's network transport: + +```swift +public init(networkTransport: NetworkTransport, + store: ApolloStore) +``` + +Apollo iOS provides the following classes that conform to the [`NetworkTransport` protocol](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/NetworkTransport.swift): + +| Class | Description | +|-------|-------------| +| `RequestChainNetworkTransport` | Passes a request through a chain of interceptors that can interact with the request both before and after it's transmitted. Uses standard HTTP requests to communicate with the server. | +| `WebSocketTransport` | Transmits _all_ GraphQL operations via WebSocket. Requires the `Apollo/WebSocket` sub-spec. | +| `SplitNetworkTransport` | Transmits subscription operations via WebSocket and other operations via HTTP. Requires the `Apollo/WebSocket` sub-spec. | + +> * For more information on `RequestChainNetworkTransport`, see [Request pipeline in Apollo iOS](/request-pipeline/). +> * For more information on `WebSocketTransport` and `SplitNetworkTransport`, see [Subscriptions](/subscriptions/). diff --git a/docs/source/request-pipeline.mdx b/docs/source/request-pipeline.mdx new file mode 100644 index 0000000000..53cddd8da4 --- /dev/null +++ b/docs/source/request-pipeline.mdx @@ -0,0 +1,636 @@ +--- +title: Request pipeline in Apollo iOS +sidebar_title: Request pipeline (advanced) +--- + +import {ExpansionPanel} from 'gatsby-theme-apollo-docs'; + +In Apollo iOS, most `ApolloClient` instances use the `RequestChainNetworkTransport` to execute GraphQL queries and mutations on a remote server. Appropriately, this network transport uses a structure called a **request chain** to process each operation in individual steps. + +> For more information on the _subscription_ request pipeline, see [Subscriptions](/subscriptions/). + +## Request chains + +A **request chain** defines a sequence of **interceptors** that handle the lifecycle of a particular GraphQL operation's execution. One interceptor might add custom HTTP headers to a request, while the next might be responsible for actually _sending_ the request to a GraphQL server over HTTP. A third interceptor might then write the operation's result to the Apollo iOS cache. + +When an operation is executed, an object called an **`InterceptorProvider`** generates a `RequestChain` for the operation. Then, `kickoff` is called on the request chain, which runs the first interceptor in the chain: + +```mermaid +flowchart TB + operation(Client executes
operation) + operation --> chain(InterceptorProvider
creates request chain) + link1(First
interceptor) + link2(Second
interceptor) + complete(Chain complete) + result(Return Result) + error(Handle Error) + chain--"kickoff called"-->link1 + link1--proceedAsync called-->link2 + link2 --> complete + complete--No result available-->error + complete--Result available-->result + class error secondary; + class result tertiary; +``` + +An interceptor can perform arbitrary, asynchronous logic on any thread. When an interceptor finishes running, it calls `proceedAsync` on its `RequestChain`, which advances to the next interceptor. + +By default when the last interceptor in the chain finishes, if a parsed operation result is available, that result is returned to the operation's original caller. Otherwise, error-handling logic is called. + +**Each request has its own short-lived `RequestChain`.** This means that the sequence of interceptors can differ for each operation. + +## Interceptor providers + +To generate a [request chain](#request-chains) for each GraphQL operation, Apollo iOS passes operations to an object called an **interceptor provider**. This object conforms to the [`InterceptorProvider` protocol](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/InterceptorProvider.swift). + +### Default provider + +`DefaultInterceptorProvider` is a default implementation of an interceptor provider. It works with the Apollo iOS parsing and caching system and tries to replicate the experience of using the old `HTTPNetworkTransport` as closely as possible. It takes a `URLSessionClient` and an `ApolloStore` to pass into the interceptors it creates. + +**`DefaultInterceptorProvider` is recommended for most applications.** If necessary, you can create a [custom interceptor provider](#custom-interceptor-providers). + +#### Default interceptors + +The `DefaultInterceptorProvider` creates a request chain with the following interceptors for _every_ operation, as shown in the [source](https://github.com/apollographql/apollo-ios/blob/57c07f1fa046b98ce86107ebacb521dd7cd9855c/Sources/Apollo/DefaultInterceptorProvider.swift#L30-L40): + + + +```mermaid +flowchart TB + max(MaxRetryInterceptor) --> cacheRead(CacheReadInterceptor) + cacheRead --> network(NetworkFetchInterceptor) + network --> response(ResponseCodeInterceptor) + response --> json(JSONResponseParsingInterceptor) + json --> apq(AutomaticPersistedQueryInterceptor) + apq --> cacheWrite(CacheWriteInterceptor) +``` + + + +> These built-in interceptors are described [below](#built-in-interceptors). + +### Custom interceptor providers + +> [See an example interceptor provider.](#example-interceptor-provider) + +If your use case requires it, you can create a custom struct or class that conforms to the [`InterceptorProvider` protocol](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/InterceptorProvider.swift). + +If you define a custom `InterceptorProvider`, it should almost always create a `RequestChain` that uses a similar structure to the [default](#default-interceptors), but that includes additions or modifications as needed for particular operations. + +> If you only need to add interceptors to the beginning or end of the default request chain, you can _subclass_ `DefaultInterceptorProvider` instead of creating a new class from scratch. + +When creating request chains in your custom interceptor provider, note the following: + +* Interceptors are designed to be **short-lived**. Your interceptor provider should provide a completely new set of interceptors for each request to avoid having multiple calls use the same interceptor instance simultaneously. +* Holding references to individual interceptors (outside of test verification) is generally not recommended. Instead, you can create an interceptor that holds onto a longer-lived object, and the provider can pass this object into each new set of interceptors. This way, each interceptor is disposable, but you don't have to recreate the underlying object that does heavier work. + +If you do create your own `InterceptorProvider`, you can use any of the built-in interceptors that are included in Apollo iOS: + +### Built-in interceptors + +Apollo iOS provides a collection of built-in interceptors you can create in a [custom interceptor provider](#custom-interceptor-providers). You can also create a custom interceptor by defining a class that conforms to the [`ApolloInterceptor` protocol](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/ApolloInterceptor.swift). + +> [See examples of custom interceptors](#example-interceptors) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
+ +**Pre-network** +
+ +##### `MaxRetryInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/MaxRetryInterceptor.swift) + + +Enforces a maximum number of retries for a GraphQL operation that initially fails (default three retries). +
+ +##### `CacheReadInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/CacheReadInterceptor.swift) + + +Reads data from the Apollo iOS cache _before_ an operation is executed on the server, according to that operation's `cachePolicy`. + +If cached data is found that fully resolves the operation, that data is returned. The request chain then continues or terminates according to the operation's `cachePolicy`. +
+ +**Network** +
+ +##### `NetworkFetchInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/NetworkFetchInterceptor.swift) + + +Takes a [`URLSessionClient`](#the-urlsessionclient-class) and uses it to send the prepared HTTPRequest (or subclass thereof) to the GraphQL server. + +If you're sending operations over the network, your `RequestChain` requires this interceptor (or a custom interceptor that handles network communication). + +
+ +**Post-network** +
+ +##### `ResponseCodeInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/ResponseCodeInterceptor.swift) + + +For unsuccessfully executed operations, checks the response code of the GraphQL server's HTTP response and passes it to the `RequestChain`'s `handleErrorAsync` callback. + +Note that most errors at the GraphQL level are returned with a `200` status code and information in the `errors` array (per the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Response-Format)). This interceptor helps with server-level errors (such as `500`s) and errors that are returned by middleware. + +For more information, see [this article on error handling in GraphQL](https://medium.com/@sachee/200-ok-error-handling-in-graphql-7ec869aec9bc). + +
+ +##### `AutomaticPersistedQueryInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/AutomaticPersistedQueryInterceptor.swift) + + +Checks a GraphQL server's response _after_ execution to see whether the provided APQ hash for the operation was successfully found by the server. If it _wasn't_, the interceptor restarts the chain and the operation is retried with the full query string. + +
+ +##### `JSONResponseParsingInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/JSONResponseParsingInterceptor.swift) + + +Parses a GraphQL server's JSON response into a `GraphQLResult` object and attaches it to the `HTTPResponse`. + +
+ +##### `CacheWriteInterceptor` + +[View source](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/CacheWriteInterceptor.swift) + + +Writes response data to the Apollo iOS cache _after_ an operation is executed on the server, according to that operation's `cachePolicy`. + +
+ +#### `additionalErrorInterceptor` + +An `InterceptorProvider` can optionally provide an `additionalErrorInterceptor` that's called before an error is returned to the caller. This is mostly useful for logging and tracing errors. This interceptor must conform to the [`ApolloErrorInterceptor` protocol](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/ApolloErrorInterceptor.swift). + +The `additionalErrorInterceptor` is _not_ part of the request chain. Instead, any other interceptor can invoke this interceptor by calling `chain.handleErrorAsync`. + +Note that for _expected_ errors with a clear resolution (such as renewing an expired authentication token), you should define an interceptor _within_ your request chain that can resolve the issue and retry the operation. + +### Interceptor flow + +Most interceptors execute their logic and then call `chain.proceedAsync` to proceed to the next interceptor in the request chain. However, interceptors can call other methods to override this default flow. + +#### Retrying an operation + +Any interceptor can call `chain.retry` to immediately restart the current request chain from the beginning. This can be helpful if the interceptor needed to refresh an access token or modify other configuration for the operation to succeed. + +> **Important:** Do not call `retry` in an unbounded way. If your server is returning `500`s or if the user has no internet connection, repeatedly retrying can create an infinite loop of requests (especially if you aren't using the `MaxRetryInterceptor` to limit the number of retries). +> +> Unbounded retries will drain your user's battery and might also run up their data usage. Make sure to only `retry` when there's something your code can do about the original failure! + +#### Returning a value + +An interceptor can directly return a value to the operation's original caller, instead of waiting for the request chain to complete. To do so, the interceptor can call `chain.returnValueAsync`. + +**This does not prevent the rest of the request chain from executing.** An interceptor can still call `chain.proceedAsync` as usual after calling `chain.returnValueAsync`. + +You can even call `chain.returnValueAsync` multiple times within a request chain! This is helpful when initially returning a _locally cached_ value before returning a value returned by the GraphQL server. + +#### Returning an error + +If an interceptor encounters an error, it can return the details of that error by calling `chain.handleErrorAsync`. + +**This does not prevent the rest of the request chain from executing.** An interceptor can still call `chain.proceedAsync` as usual after calling `chain.handleErrorAsync`. However, if the encountered error will cause the operation to fail, you can skip calling `chain.proceedAsync` to end the request chain. + +## Examples + +The following example snippets demonstrate how to use an advanced request pipeline with custom interceptors. This code assumes you have the following _hypothetical_ classes in your own code (these classes are _not_ part of Apollo iOS): + +- **`UserManager`:** Checks whether the active user is logged in, performs associated checks on errors and responses to see if they need to renew their token, and performs that renewal when necessary. +- **`Logger`:** Handles printing logs based on their level. Supports `.debug`, `.error`, and `.always` log levels. + +### Example interceptors + +#### `UserManagementInterceptor` + +This example interceptor checks whether the active user is logged in. If so, it asynchronously renews that user's access token if it's expired. Finally, it adds the access token to an `Authorization` header before proceeding to the next interceptor in the request chain. + +```swift +import Apollo + +class UserManagementInterceptor: ApolloInterceptor { + + enum UserError: Error { + case noUserLoggedIn + } + + /// Helper function to add the token then move on to the next step + private func addTokenAndProceed( + _ token: Token, + to request: HTTPRequest, + chain: RequestChain, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + request.addHeader(name: "Authorization", value: "Bearer \(token.value)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + guard let token = UserManager.shared.token else { + // In this instance, no user is logged in, so we want to call + // the error handler, then return to prevent further work + chain.handleErrorAsync(UserError.noUserLoggedIn, + request: request, + response: response, + completion: completion) + return + } + + // If we've gotten here, there is a token! + if token.isExpired { + // Call an async method to renew the token + UserManager.shared.renewToken { [weak self] tokenRenewResult in + guard let self = self else { + return + } + + switch tokenRenewResult { + case .failure(let error): + // Pass the token renewal error up the chain, and do + // not proceed further. Note that you could also wrap this in a + // `UserError` if you want. + chain.handleErrorAsync(error, + request: request, + response: response, + completion: completion) + case .success(let token): + // Renewing worked! Add the token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } + } else { + // We don't need to wait for renewal, add token and move on + self.addTokenAndProceed(token, + to: request, + chain: chain, + response: response, + completion: completion) + } + } +} +``` + +#### `RequestLoggingInterceptor` + +This example interceptor logs the outgoing request using the hypothetical `Logger` class, then proceeds to the next interceptor in the request chain: + +```swift +import Apollo + +class RequestLoggingInterceptor: ApolloInterceptor { + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + Logger.log(.debug, "Outgoing request: \(request)") + chain.proceedAsync(request: request, + response: response, + completion: completion) + } +} +``` + +#### `‌ResponseLoggingInterceptor` + +This example interceptor uses the hypothetical `Logger` class to log the request's response if it exists, then proceeds to the next interceptor in the request chain. + +This is an example of an interceptor that can both proceed _and_ throw an error. We don't necessarily want to stop processing if this interceptor was added in wrong place, but we _do_ want to know about that error. + +```swift +import Apollo + +class ResponseLoggingInterceptor: ApolloInterceptor { + + enum ResponseLoggingError: Error { + case notYetReceived + } + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void) { + + defer { + // Even if we can't log, we still want to keep going. + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + + guard let receivedResponse = response else { + chain.handleErrorAsync(ResponseLoggingError.notYetReceived, + request: request, + response: response, + completion: completion) + return + } + + Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)") + + if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) { + Logger.log(.debug, "Data: \(stringData)") + } else { + Logger.log(.error, "Could not convert data to string!") + } + } +} +``` + +### Example interceptor provider + +This `InterceptorProvider` creates request chains using all of the [default interceptors](#default-interceptors) in their usual order, with all of the [example interceptors](#example-interceptors) defined above added at the appropriate points in the request pipeline: + +```swift +import Foundation +import Apollo + +struct NetworkInterceptorProvider: InterceptorProvider { + + // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they + // will be handed to different interceptors. + private let store: ApolloStore + private let client: URLSessionClient + + init(store: ApolloStore, + client: URLSessionClient) { + self.store = store + self.client = client + } + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + MaxRetryInterceptor(), + CacheReadInterceptor(store: self.store), + UserManagementInterceptor(), + RequestLoggingInterceptor(), + NetworkFetchInterceptor(client: self.client), + ResponseLoggingInterceptor(), + ResponseCodeInterceptor(), + JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), + AutomaticPersistedQueryInterceptor(), + CacheWriteInterceptor(store: self.store) + ] + } +} +``` + +### Example `Network` singleton + +As when initializing a [basic client](/initialization/#basic-client-creation), it's recommended to create a `Network` singleton to use a single `ApolloClient` instance across your app. + +Here's what that singleton might look like for an advanced client: + +```swift +import Foundation +import Apollo + +class Network { + static let shared = Network() + + private(set) lazy var apollo: ApolloClient = { + // The cache is necessary to set up the store, which we're going to hand to the provider + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + + let client = URLSessionClient() + let provider = NetworkInterceptorProvider(store: store, client: client) + let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")! + + let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, + endpointURL: url) + + + // Remember to give the store you already created to the client so it + // doesn't create one on its own + return ApolloClient(networkTransport: requestChainTransport, + store: store) + }() +} +``` + +An example of setting up a client that can handle WebSocket and subscriptions is included in the [subscriptions documentation](subscriptions/#sample-subscription-supporting-initializer). + +## `RequestChainNetworkTransport` API reference + +The initializer for `RequestChainNetworkTransport` accepts the following properties, which provide you with fine-grained control of your HTTP requests and responses: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ +##### `interceptorProvider` + +`InterceptorProvider` + + +**Required.** The interceptor provider to use when constructing a [request chain](#request-chains). See below for details on interceptor providers. +
+ +##### `endpointURL` + +`URL` + + +**Required.** The GraphQL endpoint URL to use for all operations. +
+ +##### `additionalHeaders` + +`Dictionary` + + +Any additional HTTP headers that should be added to **every** request, such as an API key or a language setting. + +If a header should only be added to _certain_ requests, or if its value might differ between requests, you should add that header in an interceptor instead. + +The default value is an empty dictionary. +
+ +##### `autoPersistQueries` + +`Bool` + + +If `true`, Apollo iOS uses [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) (APQ) to send an operation's hash instead of the full operation body by default. + +**Note:** To use APQ, make sure to generate your types with operation identifiers. In your Swift Script, make sure to pass a non-`nil` `operationIDsURL` to have this output. Also make sure you're using the `AutomaticPersistedQueryInterceptor` in your chain after a network request has come back to handle known APQ errors. + +The default value is `false`. +
+ +##### `requestBodyCreator` + +`RequestBodyCreator` + + +The `RequestBodyCreator` object to use to build your `URLRequest`s. + +The default value is an `ApolloRequestBodyCreator` initialized with the default configuration. +
+ +##### `useGETForQueries` + +`Bool` + + +If `true`, Apollo iOS sends all query operations using `GET` instead of `POST`. Mutation operations always use `POST`. + +This can improve performance if your GraphQL server uses a CDN (Content Delivery Network) to cache the results of queries that rarely change. + +The default value is `false`. +
+ +##### `useGETForPersistedQueryRetry` + +`Bool` + + +If `true`, Apollo iOS sends a full query operation using `GET` instead of `POST` after the GraphQL server reports that an APQ hash is not available in its cache. + +The default value is `false`. +
+ +## The `URLSessionClient` class + +Because `URLSession` only supports use in the background using the delegate-based API, Apollo iOS provides a [`URLSessionClient`](https://github.com/apollographql/apollo-ios/blob/main/Sources/Apollo/URLSessionClient.swift) class that helps configure that. + +> Note that because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass `URLSessionClient`'s initializer a `URLSessionConfiguration`, **not** an existing `URLSession`. + +By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `DefaultInterceptorProvider` use the default initializer for `URLSessionClient`. + +The `URLSessionClient` class and most of its methods are `open`, so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using, or if you need to handle additional delegate scenarios.