Request pipeline in Apollo iOS
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.
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:
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 for each GraphQL operation, Apollo iOS passes operations to an object called an interceptor provider. This object conforms to the InterceptorProvider
protocol.
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.
Default interceptors
The DefaultInterceptorProvider
creates a request chain with the following interceptors for every operation, as shown in the source:
Show default request chain
These built-in interceptors are described below.
Custom interceptor providers
If your use case requires it, you can create a custom struct or class that conforms to the InterceptorProvider
protocol.
If you define a custom InterceptorProvider
, it should almost always create a RequestChain
that uses a similar structure to the default, 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. You can also create a custom interceptor by defining a class that conforms to the ApolloInterceptor
protocol.
Name | Description |
---|---|
Pre-network | |
View source | Enforces a maximum number of retries for a GraphQL operation that initially fails (default three retries). |
View source | 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 | |
View source | Takes a URLSessionClient 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 | |
View source | 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). 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. |
View source | 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. |
View source | Parses a GraphQL server's JSON response into a GraphQLResult object and attaches it to the HTTPResponse . |
View source | 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.
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 returning500
s or if the user has no internet connection, repeatedly retrying can create an infinite loop of requests (especially if you aren't using theMaxRetryInterceptor
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.
1import Apollo
2
3class UserManagementInterceptor: ApolloInterceptor {
4
5 enum UserError: Error {
6 case noUserLoggedIn
7 }
8
9 /// Helper function to add the token then move on to the next step
10 private func addTokenAndProceed<Operation: GraphQLOperation>(
11 _ token: Token,
12 to request: HTTPRequest<Operation>,
13 chain: RequestChain,
14 response: HTTPResponse<Operation>?,
15 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
16
17 request.addHeader(name: "Authorization", value: "Bearer \(token.value)")
18 chain.proceedAsync(request: request,
19 response: response,
20 completion: completion)
21 }
22
23 func interceptAsync<Operation: GraphQLOperation>(
24 chain: RequestChain,
25 request: HTTPRequest<Operation>,
26 response: HTTPResponse<Operation>?,
27 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
28
29 guard let token = UserManager.shared.token else {
30 // In this instance, no user is logged in, so we want to call
31 // the error handler, then return to prevent further work
32 chain.handleErrorAsync(UserError.noUserLoggedIn,
33 request: request,
34 response: response,
35 completion: completion)
36 return
37 }
38
39 // If we've gotten here, there is a token!
40 if token.isExpired {
41 // Call an async method to renew the token
42 UserManager.shared.renewToken { [weak self] tokenRenewResult in
43 guard let self = self else {
44 return
45 }
46
47 switch tokenRenewResult {
48 case .failure(let error):
49 // Pass the token renewal error up the chain, and do
50 // not proceed further. Note that you could also wrap this in a
51 // `UserError` if you want.
52 chain.handleErrorAsync(error,
53 request: request,
54 response: response,
55 completion: completion)
56 case .success(let token):
57 // Renewing worked! Add the token and move on
58 self.addTokenAndProceed(token,
59 to: request,
60 chain: chain,
61 response: response,
62 completion: completion)
63 }
64 }
65 } else {
66 // We don't need to wait for renewal, add token and move on
67 self.addTokenAndProceed(token,
68 to: request,
69 chain: chain,
70 response: response,
71 completion: completion)
72 }
73 }
74}
RequestLoggingInterceptor
This example interceptor logs the outgoing request using the hypothetical Logger
class, then proceeds to the next interceptor in the request chain:
1import Apollo
2
3class RequestLoggingInterceptor: ApolloInterceptor {
4
5 func interceptAsync<Operation: GraphQLOperation>(
6 chain: RequestChain,
7 request: HTTPRequest<Operation>,
8 response: HTTPResponse<Operation>?,
9 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
10
11 Logger.log(.debug, "Outgoing request: \(request)")
12 chain.proceedAsync(request: request,
13 response: response,
14 completion: completion)
15 }
16}
‌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.
1import Apollo
2
3class ResponseLoggingInterceptor: ApolloInterceptor {
4
5 enum ResponseLoggingError: Error {
6 case notYetReceived
7 }
8
9 func interceptAsync<Operation: GraphQLOperation>(
10 chain: RequestChain,
11 request: HTTPRequest<Operation>,
12 response: HTTPResponse<Operation>?,
13 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
14
15 defer {
16 // Even if we can't log, we still want to keep going.
17 chain.proceedAsync(request: request,
18 response: response,
19 completion: completion)
20 }
21
22 guard let receivedResponse = response else {
23 chain.handleErrorAsync(ResponseLoggingError.notYetReceived,
24 request: request,
25 response: response,
26 completion: completion)
27 return
28 }
29
30 Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)")
31
32 if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) {
33 Logger.log(.debug, "Data: \(stringData)")
34 } else {
35 Logger.log(.error, "Could not convert data to string!")
36 }
37 }
38}
Example interceptor provider
This InterceptorProvider
creates request chains using all of the default interceptors in their usual order, with all of the example interceptors defined above added at the appropriate points in the request pipeline:
1import Foundation
2import Apollo
3
4struct NetworkInterceptorProvider: InterceptorProvider {
5
6 // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they
7 // will be handed to different interceptors.
8 private let store: ApolloStore
9 private let client: URLSessionClient
10
11 init(store: ApolloStore,
12 client: URLSessionClient) {
13 self.store = store
14 self.client = client
15 }
16
17 func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
18 return [
19 MaxRetryInterceptor(),
20 CacheReadInterceptor(store: self.store),
21 UserManagementInterceptor(),
22 RequestLoggingInterceptor(),
23 NetworkFetchInterceptor(client: self.client),
24 ResponseLoggingInterceptor(),
25 ResponseCodeInterceptor(),
26 JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
27 AutomaticPersistedQueryInterceptor(),
28 CacheWriteInterceptor(store: self.store)
29 ]
30 }
31}
Example Network
singleton
As when initializing a basic client, 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:
1import Foundation
2import Apollo
3
4class Network {
5 static let shared = Network()
6
7 private(set) lazy var apollo: ApolloClient = {
8 // The cache is necessary to set up the store, which we're going to hand to the provider
9 let cache = InMemoryNormalizedCache()
10 let store = ApolloStore(cache: cache)
11
12 let client = URLSessionClient()
13 let provider = NetworkInterceptorProvider(store: store, client: client)
14 let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")!
15
16 let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,
17 endpointURL: url)
18
19
20 // Remember to give the store you already created to the client so it
21 // doesn't create one on its own
22 return ApolloClient(networkTransport: requestChainTransport,
23 store: store)
24 }()
25}
An example of setting up a client that can handle WebSocket and subscriptions is included in the subscriptions documentation.
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 |
---|---|
| Required. The interceptor provider to use when constructing a request chain. See below for details on interceptor providers. |
| Required. The GraphQL endpoint URL to use for all operations. |
| 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. |
| If true , Apollo iOS uses Automatic Persisted Queries (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 . |
| The RequestBodyCreator object to use to build your URLRequest s.The default value is an ApolloRequestBodyCreator initialized with the default configuration. |
| 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 . |
| 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
class that helps configure that.
Note that because setting up a delegate is only possible in the initializer for
URLSession
, you can only passURLSessionClient
's initializer aURLSessionConfiguration
, not an existingURLSession
.
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.