Apollo-client: Feature idea/Question: Accessing sent request in afterware

Created on 13 Jun 2017  路  15Comments  路  Source: apollographql/apollo-client

Hello,

I am currently considering moving from Relay to Apollo and I am a little disappointed about the way network interface is handling request and response.

My problem is that I am using JWT token to provide authentication and in order to avoid user to seize his credentials each time token expire, I simply refresh the token based on the old one.

In order to do it, here is what I was doing with Relay:
1) Some request or mutation is sent
2) The server send a response with 401 status (User token has expired)
3) This response is catch by the network layer which send a refresh token to retrieve a new token based on the old one.
4) Once the new token is retrieved, the initial request that failed is sent back to server in order to obtain the data.

Which, in Apollo approach, should be done through middleware and afterware.
But, as the afterware does not have a reference to the initial request it gets a response for, I am stucked at step 4.

Did someone find a workaround for handling this ? Or am I missing something in the way to refresh the JWT token ?

Most helpful comment

Hi @david1820,
For sure, here is my implementation of the network layer:

networkInterface.js

import { setToken } from 'services/login';
import { HTTPFetchNetworkInterface } from 'react-apollo';
import refreshTokenMutation from 'apollo/mutations/refreshToken.graphql';
import client from 'services/apollo';

class CustomNetworkInterface extends HTTPFetchNetworkInterface {
  constructor(props) {
    super(props);

    this.refreshingPromise = null;
  }

  query(request) {
    return super.query(request)
      .then((response) => {
        if (response && response.errors && response.errors[0] &&
            response.errors[0].message === 'jwt expired') {
          if (!this.refreshingPromise) {
            this.refreshingPromise =
              client.mutate({ mutation: refreshTokenMutation })
                .then(({ data }) => { setToken(data.refreshToken.token); });
          }
          return this.refreshingPromise.then(() => {
            this.refreshingPromise = null;
            return super.query(request);
          });
        }
        return response;
      });
  }
}

export default CustomNetworkInterface;

apollo.js

import { getToken } from 'services/login';
import { ApolloClient } from 'react-apollo';
import NetworkInterface from 'apollo/networkInterface';

const networkInterface = new NetworkInterface(<API_URL>);
const client = new ApolloClient({ networkInterface });

networkInterface
  .use([{
    applyMiddleware(req, next) {
      if (!req.options.headers) req.options.headers = {};  // Create the header object if needed.

      const token = getToken();
      req.options.headers.authorization = token ? `Bearer ${token}` : null;
      next();
    },
  }]);

export default client;

As you can see the retry strategy is based on a message sent by the server, it would be better to do it based on response code 401, but at this stage, I couldn't get the response with errorCode as response is already parsed and response code is not available.

All 15 comments

Hey Chris,

Thank you for reaching out! We are currently working on a new network interface(apollo-fetcher) that is composed of units called fetchers. Each fetcher receives an operation with some context and returns an Observable capable of representing the current and planned GraphQL responses. The fetcher can modify the query or context and pass them down to another fetcher, until the operation reaches a base fetcher that sends the request out over a transport.

If I understand your issue correctly, you could design a fetcher that checks the status of the response and then modifies the context to include a new JWT token. You would then create a base fetcher that checks the operation context for a JWT token that is add to the request header. Does that sound like it could help you?

Hello @evanshauser and thanks for the comprehensive answer !

I had missed the point about this refactoring being done. It seems really nice.
However, I am not sure we could properly handle this use case with this new flow because as you stated:

The fetcher can modify the query or context and pass them down to another fetcher, until the operation reaches a base fetcher that sends the request out over a transport.

So the fetcher allow work to be done before sending the query. While it would be possible to send a query to know if token has expired each time before sending a real query this would be wasteful. Instead, we should be able to add a mechanism that is executed after the query is sent (actually afterware at the moment). This would allow processing the response and, if it has 401 status, be able to send a query to refetch a new token and send back the query that failed with the new token.
Am I right or did I miss something about how fetcher would work ?

P.S: At the moment, I just solved the problem by writing my own Network Interface inheriting from HTTPFetchNetworkInterface and overriding query function, which seems a not-too-bad way to handle it.

@chris-verclytte glad you solved the issue with a NetworkInterface!

You are correct a fetcher has the ability to modify the request on the way out to the server by changing the Operation. Last post I should have mentioned that the Observable returned by the underlying fetcher provides the execution environment for responding to the result of a request.

An Observable's uses three callbacks, next, complete, and error. next is called on every new server response with the new data, so it's most relevant to your desired behavior. The next for your jwt-fetcher could check the each result for a 401 and update the JWT token in the Operation. It would then resend the Operation to the underlying fetcher.

The attached image at the bottom might help, where Operations are blue and Observables are orange. Your jwt-fetcher would be similar to the Polling Fetcher. Instead of no-cache in the context, you would pass the jwt token down and resend depending on the response code.

Currently, the fetcher spec is open for comment and might give you some more information. Your use case is exactly the type of thing we want to make easier, so your insight would be an awesome addition. If you get a chance, please add your comment on this pull request.

context

Thanks again @evanshauser for the answer. It is really clear and it perfectly makes sense. I think my use case would be totally covered by this new architecture.
Congratulations for the great work you deliver with Apollo and, for sure, if I have some insights about this feature, I will be happy to discuss it with you.

Hi, @chris-verclytte

P.S: At the moment, I just solved the problem by writing my own Network Interface inheriting from HTTPFetchNetworkInterface and overriding query function, which seems a not-too-bad way to handle it.

Please can you give me a hint how did you do it the network interface, i need to implement the same approach.

Thanks in advance.

Hi @david1820,
For sure, here is my implementation of the network layer:

networkInterface.js

import { setToken } from 'services/login';
import { HTTPFetchNetworkInterface } from 'react-apollo';
import refreshTokenMutation from 'apollo/mutations/refreshToken.graphql';
import client from 'services/apollo';

class CustomNetworkInterface extends HTTPFetchNetworkInterface {
  constructor(props) {
    super(props);

    this.refreshingPromise = null;
  }

  query(request) {
    return super.query(request)
      .then((response) => {
        if (response && response.errors && response.errors[0] &&
            response.errors[0].message === 'jwt expired') {
          if (!this.refreshingPromise) {
            this.refreshingPromise =
              client.mutate({ mutation: refreshTokenMutation })
                .then(({ data }) => { setToken(data.refreshToken.token); });
          }
          return this.refreshingPromise.then(() => {
            this.refreshingPromise = null;
            return super.query(request);
          });
        }
        return response;
      });
  }
}

export default CustomNetworkInterface;

apollo.js

import { getToken } from 'services/login';
import { ApolloClient } from 'react-apollo';
import NetworkInterface from 'apollo/networkInterface';

const networkInterface = new NetworkInterface(<API_URL>);
const client = new ApolloClient({ networkInterface });

networkInterface
  .use([{
    applyMiddleware(req, next) {
      if (!req.options.headers) req.options.headers = {};  // Create the header object if needed.

      const token = getToken();
      req.options.headers.authorization = token ? `Bearer ${token}` : null;
      next();
    },
  }]);

export default client;

As you can see the retry strategy is based on a message sent by the server, it would be better to do it based on response code 401, but at this stage, I couldn't get the response with errorCode as response is already parsed and response code is not available.

Hi @chris-verclytte, thanks for your prompt answer

I'll try, very good implementation, thanks again.

@evans @chris-verclytte So I鈥檓 currently trying to migrate from the network interface solution to an Apollo 2.0 link, but I can鈥檛 figure out how to make the client available in the link as it creates a cyclical dependency.

@MrLoh check out this post by Bee that explains how they ported this idea to conform to Apollo 2.0 link spec. I found it pretty neat. I am currently implementing the same thing and have been using that as a good approach. @chris-verclytte and @evans since this thread have you devised a better strategy to solve the problem of refreshing tokens?

@RobertWSaunders Yeah I did something very similar. I'm using the apollo client to refetch the token though, simply injecting it into the auth link.
here's a gist https://gist.github.com/MrLoh/1ae9e48ceb595207ecb3cfdb9849c083

@MrLoh are you still using technique above or something different?

still using the above. work's like a charm in production

Great! I implemented and works well so far.

  • getToken, are you retrieveing token from local storage or from Apollo store, or somewhere else?

  • so if error other and 401 it throws and then that could be handled with Apollo error link?

  1. I save the token in react-native-sensitive-info.
  2. yes exactly all other errors are passed along to the next link (if I remember correctly).

@MrLoh for me error part is not getting called
error: ( netowrkError, GraphQLError) => { console.log("---->", JSON.stringify(netowrkError)) console.log("---->", JSON.stringify(GraphQLError)) if (netowrkError.statusCode === 401) { this.refreshToken().then(success => { if (success) { // set new token and retry operation this.setTokenHeader(operation); innerSubscription = forward(operation).subscribe(observer); } else { // throw error observer.error(new Error('jwt refresh failed')); } }); } else { observer.error(netowrkError); }

Was this page helpful?
0 / 5 - 0 ratings