apollo-link-token-refresh
apollo-link-token-refresh copied to clipboard
Error with apollo version 3.0
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
Fwiw, I am seeing the same issue.
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 };
See also #17
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 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'.
I'm also seeing this issue.
queuing.js:39 Uncaught (in promise) TypeError: request.operation.toKey is not a function
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.
Looks like this may have been fixed by #18