When performing a fetch query against backend (dotnet) from client (iOS) and the current token provided in header has expired, what options are there to trigger a retry in Network?
So for now I've implemented custom logic for HTTPNetworkTransportPreflightDelegate which sets current preserved token and then custom logic for HTTPNetworkTransportRetryDelegate which should handle retries if current request fails.
If the token has expired during the request, backend response is still 200 OK, but the error is within the response body like this.
{
"errors": [
{
"message": "GraphQL.Validation.ValidationError: You are not authorized to run this query.\nThe current user must be authenticated.\n",
"locations": [
{
"line": 2,
"column": 3
}
],
"extensions": {
"code": "authorization"
}
}
]
}
This doesn't trigger the refresh logic within HTTPNetworkTransportRetryDelegate.
Can someone give hints/examples on how to do this? I'd be happy to avoid writing refresh-token logic in each viewmodel fetch response like below.
class WodCalendarViewModel {
func getTrainingPrograms(completion: @escaping ([TrainingProgramQuery.Data.TrainingProgram.Wod?]) -> Void) {
Network.shared.apollo.fetch(query: TrainingProgramQuery()) { result in
switch result {
case .success(let graphQLResult):
if let wods = graphQLResult.data?.trainingPrograms?.compactMap({ $0?.wods }).flatMap({ $0 }) {
completion(wods)
} else if let errors = graphQLResult.errors {
// GraphQL errors
print(errors)
}
case .failure(let error):
// Network or response format errors
print(error)
}
}
}
}
Thanks in advance! :)
I stumbled upon this custom HTTPNetworkTransportGraphQLErrorDelegate and implemented following logic which seems to fulfill my requirement for now.
extension Network: HTTPNetworkTransportGraphQLErrorDelegate{
func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedGraphQLErrors errors: [GraphQLError], retryHandler: @escaping (Bool) -> Void) {
if(errors.contains(where: {($0.extensions?.contains(where: {$0.value as? String == "authorization"}) ?? false)})){
guard let authState = AuthStateManager.shared.loadState() else {
retryHandler(false) // logout
return
}
authState.performAction(freshTokens: { (accessToken, idToken, error) in
guard error == nil else {
retryHandler(false) // logout
return
}
AuthStateManager.shared.saveState(authState: authState)
retryHandler(true)
return
})
}
retryHandler(false)
}
}
I also had same confusion on handling JTW token expiration/refresh. but i figured out a way to handle JTW token expiration/refresh using HTTPNetworkTransportGraphQLErrorDelegate. When error occurs receivedGraphQlErrors delegate is called so you can check status code their and call retryHandler(true) for send request with updated token. And To fetch updated token we can didCompleteRawTaskForRequest delegate of HTTPNetworkTransportTaskCompleteDelegate, where you can check for response header for new token.
@devharis Glad you found this, this was exactly what I was going to suggest - can we close this issue out?
@designatednerd @DiwakarThapa Just before we close this up, I have a question regarding updating the new token directly within HTTPNetworkTransportGraphQLErrorDelegate vs defering it to HTTPNetworkTransportTaskCompleteDelegate and extracting it from didCompleteRawTaskForRequest. What is the difference? It seems that I now found an issue where when it refreshes the token and saves it, I need to do a pull to refresh in my view to actually force a new request with the new token...
@designatednerd since some one have raised an issue to handle JTW token expiration/refresh. i wanted to add my experience on handling JTW token expiration/refresh. Handing JTW token expiration/refresh is easy for HTTPNetworkTransport but very confusion on WebSocketTransport. I was working on an application where tokens were refreshed every four minutes and subscription were implemented in such a way that when transitioning from first view to another view, the subscription on first view is cancelled and reconnected after second view is dismissed. The problem was on subscription connection due to refresh token. Somehow i fixed the problem by implementing following solution but can't figure out whether it is a google solution or not ?
This function is used to setup ApolloClient.
func setApolloClient() {
self.apollo = {
let splitNetworkTransport = SplitNetworkTransport(httpNetworkTransport: self.networkTransport, webSocketNetworkTransport: self.webSocketTransport)
return ApolloClient(networkTransport: splitNetworkTransport)
}()
}
Used WebSocketTransportDelegate to handle error from socket
extension ApolloAuthorization: WebSocketTransportDelegate {
func webSocketTransportDidConnect(_ webSocketTransport: WebSocketTransport){
}
func webSocketTransportDidReconnect(_ webSocketTransport: WebSocketTransport){
}
Whenever socket disconnects with error this delegate is called. so i fetched the new token, implemented into header and create new webSocket and reconnected after that i created new ApolloClient calling setApolloClient
func webSocketTransport(_ webSocketTransport: WebSocketTransport, didDisconnectWithError error:Error?) {
webSocketTransport.closeConnection()
var request = URLRequest(url: URL(string: self.socketURL)!)
webSocketTransport.addApolloClientHeaders(to: &request)
var headers = request.allHTTPHeaderFields ?? [String:String]()
if let auth = self.fetchAuth().first,
let authoriation = auth.accessToken,
let identifier = auth.identifier,
let refreshToken = auth.refreshToken {
headers["authorization"] = "Bearer \(authoriation)"
headers["identifier"] = identifier
headers["refresh-token"] = refreshToken
}
request.allHTTPHeaderFields = headers
let webSocket = ApolloWebSocket(request: request)
webSocketTransport.websocketDidConnect(socket: webSocket)
setApolloClient()
}
}
@devharis in my case when token used to be refreshed, the refreshed token were added to response header so i had to use didCompleteRawTaskForRequest to fetch the new token.
@DiwakarThapa Thanks for the clarification, makes sense! 馃憤 Ok to close for me atleast.
Regarding your solution on websockets, from what I read in docs you have to setup a new instance in the way you already do, to be able to set a new token.
Most helpful comment
I stumbled upon this custom
HTTPNetworkTransportGraphQLErrorDelegateand implemented following logic which seems to fulfill my requirement for now.