apollo-link-token-refresh icon indicating copy to clipboard operation
apollo-link-token-refresh copied to clipboard

Error with apollo version 3.0

Open kumarwin opened this issue 4 years ago • 8 comments

Seems this package is throwing an error with Apollo client version 3.0, but its working with 2.6 though.

Error: queuing.js:47 Uncaught (in promise) TypeError: request.operation.toKey is not a function

kumarwin avatar Nov 16 '19 19:11 kumarwin

Fwiw, I am seeing the same issue.

n8io avatar Jan 20 '20 01:01 n8io

We're using JS here, but the following is TokenRefreshLink rewritten into a single file. The problem was that the queue system was calling toKey which was removed as unreliable. The subscriptions enabled us to attach one token request to multiple pending operations. It increases the load slightly, but can be safely removed. In exchange, you may see a small uptick in token requests for simultaneous queries for the same dataset w/ the same parameters.

Making the TypeScript change

public consumeQueue(): void {
    this.queuedRequests.forEach(request => {
      const key = request.operation.toKey();
      this.subscriptions[key] =
        request.forward(request.operation).subscribe(request.subscriber);

      return () => {
        this.subscriptions[key].unsubscribe();
      };
    });

    this.queuedRequests = [];
  }

becomes

public consumeQueue(): void {
    this.queuedRequests.forEach(request => {
      request.forward(request.operation);
    });

    this.queuedRequests = [];
  }

JS (modern JavaScript) version

If you're using pure JS, I rewrote the subscription to be local to the fetching lifecycle. This means you can subscribe to the request observable if required.

import { ApolloLink, Observable } from "@apollo/client";

const throwServerError = (response, result, message) => {
  const error = new Error(message);

  error.response = response;
  error.statusCode = response.status;
  error.result = result;

  throw error;
};

const parseAndCheckResponse = (operation, accessTokenField) => response => {
  return response
    .text()
    .then(bodyText => {
      if (typeof bodyText !== "string" || !bodyText.length) {
        // return empty body immediately
        return bodyText || "";
      }

      try {
        return JSON.parse(bodyText);
      } catch (err) {
        const parseError = err;
        parseError.response = response;
        parseError.statusCode = response.status;
        parseError.bodyText = bodyText;
        return Promise.reject(parseError);
      }
    })
    .then(parsedBody => {
      if (response.status >= 300) {
        // Network error
        throwServerError(
          response,
          parsedBody,
          `Response not successful: Received status code ${response.status}`
        );
      }
      // token can be delivered via apollo query (body.data) or as usual
      if (
        !parsedBody.hasOwnProperty(accessTokenField) &&
        parsedBody.data &&
        !parsedBody.data.hasOwnProperty(accessTokenField) &&
        !parsedBody.hasOwnProperty("errors")
      ) {
        // Data error
        throwServerError(
          response,
          parsedBody,
          `Server response was missing for query '${operation.operationName}'.`
        );
      }

      return parsedBody;
    });
};

const noop = () => {};
const noopTrue = () => true;
const noopPromiseEmpty = async () => {};
const noopErr = err => console.error(err);
const noFetch = () => {
  throw new Error("You must define a handleFetch operation to get a new token");
};

class TokenRefreshLink extends ApolloLink {
  accessTokenField = "";
  fetching = false;
  isTokenValidOrUndefined = noopTrue;
  fetchAccessToken = noop;
  handleFetch = noop;
  handleResponse = noop;
  handleError = noop;
  queue = [];
  subscriptions = {};
  subkey = 0;

  constructor({
    accessTokenField = "access_token",
    isTokenValidOrUndefined = noopTrue,
    fetchAccessToken = noopPromiseEmpty,
    handleFetch = noFetch,
    handleError = noopErr,
    handleResponse = parseAndCheckResponse,
  }) {
    super();
    this.accessTokenField = accessTokenField;
    this.fetching = false;
    this.isTokenValidOrUndefined = isTokenValidOrUndefined;
    this.fetchAccessToken = fetchAccessToken;
    this.handleFetch = handleFetch;
    this.handleError = handleError;
    this.handleResponse = handleResponse;
    this.subkey = 0;
    this.subscriptions = {};
    this.queue = [];
  }

  enqueue = req => {
    const copy = { ...req };
    copy.observable =
      copy.observable ||
      new Observable(ob => {
        this.queue.push(copy);
        if (typeof copy.subscriber === "undefined") {
          copy.subscriber = {};
          copy.subscriber.next = copy.next || ob.next.bind(ob);
          copy.subscriber.error = copy.error || ob.error.bind(ob);
          copy.subscriber.complete = copy.complete || ob.complete.bind(ob);
        }
      });
    return copy.observable;
  };

  dequeueAll = () => {
    for (const request of this.queue) {
      const id = this.subkey++;
      this.subscriptions[id] = request
        .forward(request.operation)
        .subscribe(request.subscriber);
    }

    for (const id of Object.keys(this.subscriptions)) {
      this.subscriptions[id].unsubscribe();
    }
    this.subkey = 0;
    this.subscriptions = {};
    this.queue = [];
  };

  request = (operation, forward) => {
    if (typeof forward !== "function") {
      throw new Error(
        "[Token Refresh Link]: Token Refresh Link is non-terminating link and should not be the last in the composed chain"
      );
    }
    // If token does not exists, which could means that this is a not registered
    // user request, or if it is does not expired -- act as always
    if (this.isTokenValidOrUndefined()) {
      return forward(operation);
    }

    if (!this.fetching) {
      this.fetching = true;
      this.fetchAccessToken()
        .then(this.handleResponse(operation, this.accessTokenField))
        .then(body => {
          const token = this.extractToken(body);

          if (!token) {
            throw new Error(
              "[Token Refresh Link]: Unable to retrieve new access token"
            );
          }
          return token;
        })
        .then(this.handleFetch)
        .then(() => {
          this.fetching = false;
          this.dequeueAll();
        })
        .catch(this.handleError);
    }

    return this.enqueueRequest({ operation, forward });
  };

  /**
   * An attempt to extract token from body.data. This allows us to use apollo query
   * for auth token refreshing
   * @param body {Object} response body
   * @return {string} access token
   */
  extractToken = body => {
    if (body.data) {
      return body.data[this.accessTokenField];
    }
    return body[this.accessTokenField];
  };
}

export { TokenRefreshLink };

jakobo avatar Feb 13 '20 23:02 jakobo

See also #17

icco avatar Apr 05 '20 02:04 icco

Posted an initial take on a Apollo 3.0 compatible release here: https://github.com/onpaws/apollo-link-token-refresh/tree/apollo-3.0-no-subscriptions Looking for feedback if you have time. Still want to get tests updated but tentatively looking good so far, we've got two reports of it working.

git clone [email protected]:onpaws/apollo-link-token-refresh.git
git checkout apollo-3.0-no-subscriptions

Suggestion would be to build it locally and npm/yarn link it into whatever project you're using Apollo 3.0 in.

onpaws avatar Apr 11 '20 14:04 onpaws

@onpaws First of all, thank you so much for the awesome commits!

I'm getting this error:

Type 'TokenRefreshLink' is not assignable to type 'ApolloLink'.
  Types of property 'split' are incompatible.
    Type '(test: (op: import("~/node_modules/@apollo/client/link/core/types").Operation) => boolean, left: import("~/node_modules/@apollo/client/link/core/ApolloLink").ApolloLink | import("~/node_modules/@apollo/client/link/core/types").RequestHa...' is not assignable to type '(test: (op: import("~/node_modules/apollo-link/lib/types").Operation) => boolean, left: import("~/node_modules/apollo-link/lib/link").ApolloLink | import("~/node_modules/apollo-link/lib/types").RequestHandler, right?: import("/Users/sor...'.
      Types of parameters 'test' and 'test' are incompatible.
        Types of parameters 'op' and 'op' are incompatible.
          Property 'toKey' is missing in type 'import("~/node_modules/@apollo/client/link/core/types").Operation' but required in type 'import("~/node_modules/apollo-link/lib/types").Operation'. 

typeboi avatar Apr 30 '20 00:04 typeboi

I'm also seeing this issue.

queuing.js:39 Uncaught (in promise) TypeError: request.operation.toKey is not a function

miller-productions avatar May 15 '20 08:05 miller-productions

Apollo 3.x no longer supports request.operation.toKey which is the point of https://github.com/onpaws/apollo-link-token-refresh/tree/apollo-3.0-no-subscriptions. Hard to say for sure but I guess you have a 2.x release of some Apollo package hanging around in your project. You may be able to resolve it via resolutions in package.json, but probably a better solution is to carefully check your lockfile to make sure everything is Apollo 3.x.

onpaws avatar May 15 '20 10:05 onpaws

Looks like this may have been fixed by #18

miller-productions avatar Jun 02 '20 23:06 miller-productions