Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to setup an async call with the HTTPNetworkTransportPreflightDelegate? #1245

Closed
jesster2k10 opened this issue Jun 5, 2020 · 12 comments
Closed
Labels
apollo-websockets networking-stack question Issues that have a question which should be addressed

Comments

@jesster2k10
Copy link

I'm currently trying to set up token authentication with the Apollo iOS library. I've looked through the docs here which suggests the following code:

extension Network: HTTPNetworkTransportPreflightDelegate {
  func networkTransport(_ networkTransport: HTTPNetworkTransport, 
                        willSend request: inout URLRequest) {
                        
    // Get the existing headers, or create new ones if they're nil
    var headers = request.allHTTPHeaderFields ?? [String: String]()

    // Add any new headers you need
    headers["Authorization"] = "Bearer \(UserManager.shared.currentAuthToken)"
  
    // Re-assign the updated headers to the request.
    request.allHTTPHeaderFields = headers
    
    Logger.log(.debug, "Outgoing request: \(request)")
  }
}

The issue I'm having is that my currentAuthToken method requires a callback since it's asynchronous and in the example given the code is synchronous.

Right now, my "hacky" workaround is to do something like this:

class Network {
// Other irrelevant stuff
  var accessToken: String?

func query<Query: GraphQLQuery>(_ query: Query,
    cachePolicy: CachePolicy = .returnCacheDataElseFetch,
    queue: DispatchQueue = .main,
    handler: GraphQLResultHandler<Query.Data>? = nil
  ) {
   // The asynchronous call
    Authenticator.shared.getAccessToken { [weak self] (token) in
      self?.accessToken = token
      self?.apollo.fetch(query: query, cachePolicy: cachePolicy, queue: queue, resultHandler: handler)
    }
  }

which runs the getAccessToken in a custom wrapper around the ApolloClient.fetch(query) method and sets it to an instance variable which is then accessed here:

func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
    var headers = request.allHTTPHeaderFields ?? [String: String]()
    headers["Authorization"] = "Bearer \(accessToken ?? "")"
    request.allHTTPHeaderFields = headers
    
    log.debug("Sending request with \(accessToken ?? "no token")")
    log.debug("Outgoing request: \(request)")
  }

I'm not sure if this is the best solution, but I have tried it so far and it works as expected. If there could be any clarity regarding this that would be great.

@jesster2k10
Copy link
Author

The same issue goes for setting up subscriptions as the magicToken code is again synchronous and I'm sure I'm not the only person who would be working with an async token-getter function

  private lazy var webSocketTransport: WebSocketTransport = {
    let url = URL(string: "ws://localhost:8080/websocket")!
    let request = URLRequest(url: url)
    let authPayload = ["authToken": magicToken]
    return WebSocketTransport(request: request, connectingPayload: authPayload)
  }()

@jesster2k10
Copy link
Author

Sorry I hadn't taken the time to search before opening this- #834 (comment)

@jesster2k10
Copy link
Author

I looked over at the other issue and I still wasn't able to get it solved. My sync function looks like this:

func getAccessTokenSync() -> String? {
    let semaphore = DispatchSemaphore(value: 0)
    var authToken: String?
    
    DispatchQueue.global(qos: .background).async {
      self.getAccessToken { (token) in
        authToken = token
        semaphore.signal()
      }
      
      print(authToken)
      semaphore.wait()
    }
    
    return authToken
  }

but the authToken returned is always nil. When I don't run on the background thread the app blocks

@jesster2k10
Copy link
Author

Calling it here:

  func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
    var headers = request.allHTTPHeaderFields ?? [String: String]()
    let token = Authenticator.shared.getAccessTokenSync() ?? ""
    
    headers["Authorization"] = "Bearer \(token)"
    request.allHTTPHeaderFields = headers
    
    log.debug("Sending request with token \(token)")
    log.debug("Outgoing request: \(request)")
  }

@designatednerd
Copy link
Contributor

For the web socket issue we added a way to update the headers in 0.28.0 via #1224, but you also could do the call to the async API before starting the setup of your web socket transport. For example:

func setupWebSocket(with magicToken: String) -> WebSocketTransport {
    // existing code in your lazy getter
}

self.getAccessToken() { token in 
   let webSocket = self.setupWebSocket(with: token)
   // proceed with setting up apollo client
}

@designatednerd
Copy link
Contributor

It looks like with your code in this comment you're doing an additional dispatch to a background queue that never calls back to the main queue, so the `wait() may be getting called after the initial method returns.

I think if you just ditch the DispatchQueue.global bit, it should still work.

@designatednerd designatednerd added apollo-websockets networking-stack question Issues that have a question which should be addressed labels Jun 5, 2020
@jesster2k10
Copy link
Author

Hey, so I have this setup currently:

extension Network: HTTPNetworkTransportPreflightDelegate {
  func networkTransport(_ networkTransport: HTTPNetworkTransport, shouldSend request: URLRequest) -> Bool {
    return true
  }
  
  func networkTransport(_ networkTransport: HTTPNetworkTransport, willSend request: inout URLRequest) {
    let semaphore = DispatchSemaphore(value: 0)
    var authToken: String?
    
    Authenticator.shared.getAccessToken { (token) in
      authToken = token
      semaphore.signal()
    }
      
    print(authToken)
    semaphore.wait()

    if let token = authToken {
      var headers = request.allHTTPHeaderFields ?? [String: String]()
      headers["Authorization"] = "Bearer \(token)"
      request.allHTTPHeaderFields = headers
    }

    log.debug("Sending request with token \(authToken ?? "no token")")
    log.debug("Outgoing request: \(request)")
  }
}

however, the callback function is never called for some reason which means the semaphore.signal() is never called either causing the UI thread to be blocked indefinitely.

When I remove the DispatchSemaphore code, the callback is reached but it's not assigned to local variable making it absent in the request

@designatednerd
Copy link
Contributor

designatednerd commented Jun 6, 2020

Ahhh ok that's why you added the Dispatch - even though getAccessToken is async, it appears to still be using the same thread.

What about going back to using the Dispatch, but putting semaphore.wait outside the dispatch queue:

func getAccessTokenSync() -> String? {
    let semaphore = DispatchSemaphore(value: 0)
    var authToken: String?
    
    DispatchQueue.global(qos: .background).async {
      self.getAccessToken { (token) in
        authToken = token
        semaphore.signal()
      }
      
    }
   
    semaphore.wait()
    print(authToken)

    return authToken
  }

that starts the wait on the thread before the return, so that it has to get the signal from the background thread before it proceeds, but since the access token is being retrieved on a different thread, it shouldn't be blocked by the wait.

@jesster2k10
Copy link
Author

Ahhh ok that's why you added the Dispatch - even though getAccessToken is async, it appears to still be using the same thread.

What about going back to using the Dispatch, but putting semaphore.wait outside the dispatch queue:

func getAccessTokenSync() -> String? {

    let semaphore = DispatchSemaphore(value: 0)

    var authToken: String?

    

    DispatchQueue.global(qos: .background).async {

      self.getAccessToken { (token) in

        authToken = token

        semaphore.signal()

      }

      

    }

   

    semaphore.wait()

    print(authToken)



    return authToken

  }

that starts the wait on the thread before the return, so that it has to get the signal from the background thread before it proceeds, but since the access token is being retrieved on a different thread, it shouldn't be blocked by the wait.

So far from my basic testing, it’s worked like a charm. I’ll take a more detailed look at it tomorrow but it works as expected.

I think adding in a callback to the willSend function in later version of the client would be a good idea making common implementations like this simpler.

I’ll probably try to cache the result to prevent having to make that call every time since the value won’t be changing often.

@designatednerd
Copy link
Contributor

We're going to be working on changes to the networking stack later in the year that will (hopefully) make this and any other async operation a hell of a lot easier.

That said, I think we've addressed your question for the time being, do you mind if we close this issue out?

@jesster2k10
Copy link
Author

Thank you for your help!

@designatednerd
Copy link
Contributor

You're welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
apollo-websockets networking-stack question Issues that have a question which should be addressed
Projects
None yet
Development

No branches or pull requests

2 participants