How to add headers to the ApolloClient on every graphQL call?

I was using v0.33 and recently updated it to the latest version of Apollo SDK and facing an issue:

In version 0.33 we were using a protocol HTTPNetworkTransportPreflightDelegate and its delegate methods were get called on every graphql calls and hence I add the latest token as headers in the calls-

class GraphQL: NSObject {
    func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool {
        return true
    }

    func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
        var headers = request.allHTTPHeaderFields ?? [String: String]()
        headers["Authorization"] = "Bearer <token>"
        headers["X-STATE"] = StaticClass.state
        headers["X-GEO"] = ""
        request.allHTTPHeaderFields = headers
    }
}

And I was creating the Apollo client like this:

class GraphQL: NSObject {
    lazy var networkTransport: HTTPNetworkTransport = {
        let transport = HTTPNetworkTransport(url: URL(string: EndPoints.graphQLURL)!)
        transport.delegate = self
        return transport
    }()

    lazy var apollo = ApolloClient(networkTransport: self.networkTransport)

    func performMutation() { ... }
    // Other class methods below
}

Problem -
After updating it to the latest version the protocol has been removed and hence I can not add headers with latest token in every graphql calls. My token gets change every 20 min.

This is how I am using the code now:

class GraphQL: NSObject {
    private(set) lazy var apollo: ApolloClient = {
        let url = URL(string: EndPoints.graphQLURL)!

        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = addHeaders() // Add your headers here

        let client = URLSessionClient(sessionConfiguration: configuration)
        let store = ApolloStore(cache: InMemoryNormalizedCache())
        let provider = DefaultInterceptorProvider(client: client, store: store)
        let networkTransport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url)

        return ApolloClient(networkTransport: networkTransport, store: store)
    }()

    func addHeaders() -> [AnyHashable: Any] {
        var headers = [AnyHashable: Any]()
        headers["Authorization"] = "Bearer <token>"
        headers["X-STATE"] = StaticClass.state
        headers["X-GEO"] = ""
        return headers
    }

     func performMutation() { ... }
}

All my graphql methods including initialization and all are in a class name GraphQL and I am using a static var to call these methods:

class GlobalClass {
    static var graphQL = GraphQL()

    // Other stuff
}

Calling like this:

GlobalClass.graphQL.apollo.performMutation()

How can I add / update my token as headers to the ApolloClient object for every call I make?

I would probably use a custom Interceptor and insert it into a custom RequestChain to set the token on each request.

Check out the documentation on advanced network configuration for more information on how to create these. If you have any more questions after reading that documentation, feel free to comment here and I’ll answer them!

Hello @AnthonyMDev - Thank you for your quick answer. I did implemented the custom Interceptor as per your suggestion. But we are getting below error when we try to login:

Error: login The operation couldn’t be completed.(MyApp.UserManagementInterceptor.UserError error 0.)  -->

This is how I implemented the custom Interceptor:

private(set) lazy var apollo: ApolloClient = {
        let store = ApolloStore(cache: InMemoryNormalizedCache())
        let client = URLSessionClient()
        let provider = NetworkInterceptorProvider(store: store, client: client)

        let url = URL(string: EndPoints.graphQLURL)!
        let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url)
        return ApolloClient(networkTransport: requestChainTransport, store: store)
    }()

Custom Interceptor class:

class UserManagementInterceptor: ApolloInterceptor {
    enum UserError: Error {
        case noUserLoggedIn
    }

    public var id: String = UUID().uuidString // Required stub for the ApolloInterceptor

    /// Helper function to add the token then move on to the next step
    private func addTokenAndProceed<Operation: GraphQLOperation>(
        _ token: String,
        to request: HTTPRequest<Operation>,
        chain: RequestChain,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) {
        request.addHeader(name: "Authorization", value: "Bearer \(loginToken)")
        chain.proceedAsync(
            request: request,
            response: response,
            interceptor: self,
            completion: completion
        )
    }

    func interceptAsync<Operation: GraphQLOperation>(
        chain: RequestChain,
        request: HTTPRequest<Operation>,
        response: HTTPResponse<Operation>?,
        completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) {
        if STXClass.isLoggedIn == false {
            // In this instance, no user is logged in, so we want to call
            // the error handler, then return to prevent further work
            Utils.printSTXLog("===> User not logged in")
            chain.handleErrorAsync(
                UserError.noUserLoggedIn,
                request: request,
                response: response,
                completion: completion
            )
            return
        }
        
        // If we've gotten here, there is a token!
        if Utils.isTokenInvaild() {
            // Call an async method to renew the token
            STXClass.graphQLCalls.refreshTokenServiceCall {
                Utils.printSTXLog("===> Renew token success")
                    // Renewing worked! Add the token and move on
                    self.addTokenAndProceed(
                        STXClass.userInfo.loginToken,
                        to: request,
                        chain: chain,
                        response: response,
                        completion: completion
                    )
            }
        } else {
            // We don't need to wait for renewal, add token and move on
            Utils.printSTXLog("===> Token already valid")
            addTokenAndProceed(
                STXClass.userInfo.loginToken,
                to: request,
                chain: chain,
                response: response,
                completion: completion
            )
        }
    }
}

struct NetworkInterceptorProvider: InterceptorProvider, ApolloErrorInterceptor {
    
    // 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<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
        return [
            MaxRetryInterceptor(),
            CacheReadInterceptor(store: self.store),
            UserManagementInterceptor(), // Custom Interceptor made by us
            NetworkFetchInterceptor(client: self.client),
            ResponseCodeInterceptor(),
            JSONResponseParsingInterceptor(),
            AutomaticPersistedQueryInterceptor(),
            CacheWriteInterceptor(store: self.store)
        ]
    }
    
    func handleErrorAsync<Operation>(error: any Error, chain: any Apollo.RequestChain, request: Apollo.HTTPRequest<Operation>, response: Apollo.HTTPResponse<Operation>?, completion: @escaping (Result<Apollo.GraphQLResult<Operation.Data>, any Error>) -> Void) where Operation : ApolloAPI.GraphQLOperation {
        Utils.printSTXLog("===> \(response?.httpResponse.description) -- Error => \(error)")
    }
}

The ApolloErrorInterceptor protocol I have added recently to see if I can get some better error logs but not sure it helps the way I implemented.

The moment I call Perform method of Apollo I am getting error which I shared above.

Hi @Mayank_Mathur, the returned error doesn’t look like anything Apollo iOS generates so I’d suggest adding a few breakpoints in the interceptors to see which of them are getting triggered. Alternatively you could use something like Charles Proxy to inspect the traffic to see if the operation is actually being sent to the server.

Hello @calvincestari , thank you for responding. The error I shared above is 100% from Apollo only and it is coming when I try to use custom Interceptor only as suggested earlier to me by @AnthonyMDev. When I try to initialize ApolloClient with default interceptor like below we have no issue:

private(set) lazy var apollo: ApolloClient = {
        let url = URL(string: EndPoints.graphQLURL)!
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = addHeaders() // Add your headers here

        let client = URLSessionClient(sessionConfiguration: configuration)
        let store = ApolloStore(cache: InMemoryNormalizedCache())
        let provider = DefaultInterceptorProvider(client: client, store: store)
        let networkTransport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url)

        return ApolloClient(networkTransport: networkTransport, store: store)
    }()

However when we use custom interceptor we are getting this issue:

private(set) lazy var apollo: ApolloClient = {
        let store = ApolloStore(cache: InMemoryNormalizedCache())
        let client = URLSessionClient()
        let provider = NetworkInterceptorProvider(store: store, client: client)

        let url = URL(string: EndPoints.graphQLURL)!
        let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url)
        return ApolloClient(networkTransport: requestChainTransport, store: store)
    }()

The error comes when I try to perform the mutation on Apollo. The method is:

@discardableResult
  public func perform<Mutation: GraphQLMutation>(
    mutation: Mutation,
    publishResultToStore: Bool = true,
    contextIdentifier: UUID? = nil,
    context: RequestContext? = nil,
    queue: DispatchQueue = .main,
    resultHandler: GraphQLResultHandler<Mutation.Data>? = nil
  ) -> Cancellable {
    return self.networkTransport.send(
      operation: mutation,
      cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
      contextIdentifier: contextIdentifier,
      context: context,
      callbackQueue: queue,
      completionHandler: { result in
        resultHandler?(result)
      }
    )
  }

The returned result is returning me the error -

UserManagementInterceptor.UserError error 0

If you read the custom interceptor class I have taken from the Apollo docs example it does use an enum named UserError

enum UserError: Error {
        case noUserLoggedIn
    }

That’s my point, the error is being returned from your custom interceptor, not standard Apollo iOS code which makes it difficult to diagnose remotely.

Do you know if the operation is being sent to the server or it’s not even getting that far? From your custom interceptor code it looks like the only place UserError.noUserLoggedIn can be returned from is if the user is not actually logged in - have you checked whether this is the code path being executed and leading to the error?

        if STXClass.isLoggedIn == false {
            // In this instance, no user is logged in, so we want to call
            // the error handler, then return to prevent further work
            Utils.printSTXLog("===> User not logged in")
            chain.handleErrorAsync(
                UserError.noUserLoggedIn,
                request: request,
                response: response,
                completion: completion
            )
            return
        }