Open crazy4groovy opened 6 years ago
If you're more familiar with promises, you can use fromPromise
helper
import { fromPromise } from 'apollo-link';
return fromPromise(refreshToken().then(token => {
operation.setContext({
headers: {
...oldHeaders,
authorization: token,
},
});
return forward(operation);
}))
@thymikee Tried your solution and it fails with the following message:
Uncaught (in promise) Error: Network error: Error writing result to store for query:
query UserProfile($id: ID!) {
UserProfile(id: $id) {
id
email
first_name
last_name
activated
created_at
updated_at
last_active
roles {
id
name
__typename
}
permissions {
name
value
__typename
}
profile {
address
secondary_email
phone {
id
number
type {
id
name
__typename
}
__typename
}
__typename
}
__typename
}
}
Cannot read property 'UserProfile' of undefined
at new ApolloError (ApolloError.js:43)
at QueryManager.js:327
at QueryManager.js:759
at Array.forEach (<anonymous>)
at QueryManager.js:758
at Map.forEach (<anonymous>)
at QueryManager.webpackJsonp../node_modules/apollo-client/core/QueryManager.js.QueryManager.broadcastQueries (QueryManager.js:751)
at QueryManager.js:254
Further inspection shows that the Apollo link's onError
is called twice when using the above code. Even limiting the refresh token
promise to be run once, doesn't fix the error.
What happens is:
1) Initial query is executed
2) It fails and runs apollo's link onError
3) ?? It runs apollo's link onError
again
4) Promise to refresh token, in onError
finishes executing and is resolved.
5) (The initial query isn't executed a second time after the promise is successful)
6) Initial query returns result containing data
as undefined
Here's to hoping someone finds a solution to this, else we'll have to revert to using long-lived access tokens rather than refreshing them upon expiry.
If your token retrieving logic is correct, onError
should only be called once. Looks like you have issues with your token query
@thymikee Switched out async request with a dummy promise. Still fails with the above message and the initial query is not run twice. All tokens are valid at time of test.
Code:
return fromPromise(
new Promise((resolve) => {
let headers = {
//readd old headers
...operation.getContext().headers,
//switch out old access token for new one
authorization: `Bearer mynewaccesstoken`,
};
operation.setContext({
headers
});
return resolve(forward(operation));
})
)
Edit: Removed the fromPromise
and it works correctly. Somehow, the link stack's processing ends before returning the result so forward(operation)
isn't executed.
After analysing the fromPromise
code and the commit #172 , the fromPromise
can only be used in conjecture with an Apollo Link object.
Upon researching a solution, i finally stumbled upon this project: apollo-link-token-refresh
My apollo link stack is now as follows:
[
refreshTokenLink,
requestLink,
batchHttpLink
]
refreshTokenLink
is always called to check on the access token before executing any response to the graphql endpoint and works like a charm.
Unfortunately, this assumes that the call to the graphql endpoint must always be authenticated (which it is, in my case).
It looks like onError
callback doesn't accept aync
function or Promise
returns. See code https://github.com/apollographql/apollo-link/blob/59abe7064004b600c848ee7c7e4a97acf5d230c2/packages/apollo-link-error/src/index.ts#L60-L74
This issue was reported before: #190
I think it would work better if apollo-link-error
can deal with Promise
, similar to what apollo-link-retry
do here: #436
having the same issue, using apollo with react native I need to remove some token from AsyncStorage onError so it needs to be an async function
This solution worked for me: https://stackoverflow.com/a/51321068/60223
I solved this by creating a utility promiseToObservable.js
:
import { Observable } from 'apollo-link';
export default promise =>
new Observable((subscriber) => {
promise.then(
(value) => {
if (subscriber.closed) return;
subscriber.next(value);
subscriber.complete();
},
err => subscriber.error(err)
);
return subscriber; // this line can removed, as per next comment
});
and then
import { onError } from 'apollo-link-error';
import promiseToObservable from './promiseToObservable';
export default (refreshToken: Function) =>
onError(({
forward,
graphQLErrors,
networkError = {},
operation,
// response,
}) => {
if (networkError.message === 'UNAUTHORIZED') { // or whatever you want to check
// note: await refreshToken, then call its link middleware again!
return promiseToObservable(refreshToken()).flatMap(() => forward(operation));
}
});
@crazy4groovy thanks for your example, it really helps. However, there's a small issue with it: subscriber
is invalid return value according to Observable
typings: it should rather be ZenObservable.SubscriptionObserver
:
export declare type Subscriber<T> = ZenObservable.Subscriber<T>;
export declare const Observable: {
new <T>(subscriber: Subscriber<T>): Observable<T>;
};
export declare namespace ZenObservable {
interface SubscriptionObserver<T> {
closed: boolean;
next(value: T): void;
error(errorValue: any): void;
complete(): void;
}
type Subscriber<T> = (observer: SubscriptionObserver<T>) => void | (() => void) | Subscription;
}
i.e. it's safe to return undefined instead. I guess it should be mentioned in project's README file.
UPD: I added a PR about this: https://github.com/apollographql/apollo-link/pull/825
This issue ranks high on Google so I'm sharing my solution here to help out some folks: https://gist.github.com/alfonmga/9602085094651c03cd2e270da9b2e3f7
I have tried your solution but I'm facing new problem:
Argument of type '(this: Observable<{}>, observer: Subscriber<{}>) => Observable<{}> | Promise<{}>' is not assignable to parameter of type '(this: Observable<{}>, subscriber: Subscriber<{}>) => TeardownLogic'.
Type 'Observable<{}> | Promise<{}>' is not assignable to type 'TeardownLogic'.
Type 'Observable<{}>' is not assignable to type 'TeardownLogic'.
Property 'unsubscribe' is missing in type 'Observable<{}>' but required in type 'Unsubscribable'
How are you guys storing the new auth token once it's refreshed?
Sure, I can set new headers in the retry request, however the original access token (which I'm storing in cookies) doesn't get updated which means that every single request to the server will be using the old access token (and subsequently will need to be refreshed yet again).
For some reason I'm getting the following error message whenever I try to update the cookies during the refresh (I created a new issue here about it):
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at setCookie (/root/SimplyTidyAdmin/node_modules/nookies/dist/index.js:98:17)
at /root/SimplyTidyAdmin/.next/server/static/CAhshxrRWHVF6Gzbce~pU/pages/_app.js:1273:63
at process._tickCallback (internal/process/next_tick.js:68:7)
@StupidSexyJake maybe this would help you https://stackoverflow.com/questions/55356736/change-apollo-client-options-for-jwt-token I run into a similar issue on how to update the token
Hi, thanks @crazy4groovy. I've tried your solution but I'm still having the problem, that the middleware where I append the token to graphql request is called before the new token is set to the request. Hence, the header still has the invalid token.
A bit background info: We get a network error, when token is invalid, and via a refresh token, we can get a new one and retry. But since middleware is called before the refresh token is gathered and set to local storage, it still has the invalid one. Refresh token logic works fine, since we then get the new token set in the end. I've debugged the issue a bit and timing is as follows:
onError
it is handled via the promiseToObservable
logic and retried.onRefreshToken
promise, the middleware is already in the second run with the old token.Here's a snippet of these parts (skipping onRefreshtoken. It's an async function, returning a Promise):
const promiseToObservable = (promise: Promise<any>) =>
new Observable((subscriber: any) => {
promise.then(
value => {
console.log(subscriber);
if (subscriber.closed) return;
subscriber.next(value);
subscriber.complete();
},
err => subscriber.error(err)
);
});
const authMiddleware = setContext((operation: GraphQLRequest) => {
const token = localStorage.getItem('ca_token');
return {
headers: {
...(token && !isSkipHeader(operation)
? { authorization: `Bearer ${token}` }
: {})
}
};
});
const errorLink = onError(
({
networkError,
graphQLErrors,
operation,
forward
}: ErrorResponse): any => {
if (networkError) {
switch (networkError.statusCode) {
case 401:
console.warn('Refreshing token and trying again');
// await refreshToken, then call its link middleware again
return promiseToObservable(onRefreshToken(client.mutate)).flatMap(() => forward(operation));
default:
// Handle all other errors here. Irrelevant here.
}
}
if (graphQLErrors) {
// Handle gql errors, irrelevant here.
}
}
);
Could you please tell me what I missing here? Thanks a lot in advance...
OK, sorry for the confusion, if any...
I've found the answer and it's a stupid one after looking for it for hours and finding - of course - after posting here: during the initialisation of apollo client, I've swapped middleware and error link. Now it works. Error link should be first, obviously..
old: link: from([authMiddleware, errorLink, /* others */])
new: link: from([errorLink, authMiddleware, /* others */])
Sorry again..
Hello guys,
I have the following problem using onError for refresh tokens. For the purpose of SSR using nextjs i am gathering data from all graphql queries, but what happens when we have 2 queries for example and each of them ends up with an error because jwt token is expired. Then it fires twice the onError and we are calling twice for refresh tokens which is expensive. I can't figure out where the problem might come from. Here is the code that I'm using. Can you please help with this.
https://gist.github.com/shaxaaa/15817f1bcc7b479f3c541383d2e83650
I wrestled with this problem for a bit, but I finally got it working. I threw a package together.
https://github.com/baleeds/apollo-link-refresh-token
The primary difference between this package and the one called apollo-link-token-refresh is that this package will wait for a network error before attempting a refresh.
Let me know if you guys have ideas for changes.
Here's the basic usage:
const refreshTokenLink = getRefreshTokenLink({
authorizationHeaderKey: 'Authorization',
fetchNewAccessToken,
getAccessToken: () => localStorage.getItem('access_token'),
getRefreshToken: () => localStorage.getItem('refresh_token'),
isAccessTokenValid: accessToken => isTokenValid(accessToken),
isUnauthenticatedError: graphQLError => {
const { extensions } = graphQLError;
if (
extensions &&
extensions.code &&
extensions.code === 'UNAUTHENTICATED'
) {
return true;
}
return false;
},
});
I solved this by creating a utility
promiseToObservable.js
:import { Observable } from 'apollo-link'; export default promise => new Observable((subscriber) => { promise.then( (value) => { if (subscriber.closed) return; subscriber.next(value); subscriber.complete(); }, err => subscriber.error(err) ); return subscriber; // this line can removed, as per next comment });
and then
import { onError } from 'apollo-link-error'; import promiseToObservable from './promiseToObservable'; export default (refreshToken: Function) => onError(({ forward, graphQLErrors, networkError = {}, operation, // response, }) => { if (networkError.message === 'UNAUTHORIZED') { // or whatever you want to check // note: await refreshToken, then call its link middleware again! return promiseToObservable(refreshToken()).flatMap(() => forward(operation)); } });
I use it and found it still use old token after refresh token request. so, i try as follow:
return promiseToObservable(refreshToken()).flatMap((value) => {
operation.setContext(({ headers = {} }) => ({
headers: {
// re-add old headers
// ...headers,
Authorization: `JWT ${value.token}`
}
}));
return forward(operation)
});
and It works.
However, It still has a problem that if i add the ...headers
(means re-add old headers), there is something wrong before the forward request sented:
ERROR Error: Network error: Cannot read property 'length' of null
I think the Authorization in ...headers may conflicts with new Authorization.
the problem above is in apollo-angular "apollo-angular-link-http": "^1.6.0",
and not in apollo-client "apollo-link-http": "^1.5.16",
while link-error is the same"apollo-link-error": "^1.1.12",
another syntax :eyes:
import Vue from 'vue'
import { Observable } from 'apollo-link'
import { onError } from 'apollo-link-error'
const onGraphqlError = async ({ graphQLErrors = [], observer, operation, forward }) => {
// here you could call the refresh query in case you receive an expired error
for (let error of graphQLErrors)
observer.next(forward(operation)) // this line would retry the operation
}
const onNetworkError = async ({ observer, networkError, operation, forward }) => { }
export const errorHandler = opt => new Observable(async observer => {
try {
const payload = { ...opt, observer }
await Promise.all([onGraphqlError(payload), onNetworkError(payload)])
if (observer.closed) return
observer.complete()
} catch (error) {
observer.error(error)
}
})
Hi! Im using full websocket transport, need to request token query. No idea how to do that.
I want to do a receive request when the server responds that the accessToken
has expired.
import { onError } from "apollo-link-error";
import gql from 'graphql-tag'
// Client: VUE APOLLO
const q = {
query: gql`query token { token { accessToken } }`,
manual: true,
result({ data, loading }) {
if (!loading) {
console.log(data)
}
},
}
const link = onError(({ graphQLErrors, networkError, operation, response, forward }) => {
if (networkError) {
switch (networkError.message) {
case 'accessTokenExpired':
console.log('accessTokenExpired')
return forward(q) // NOT WORKS, NEED HELP
case 'unauthorized':
return console.log('unauthorized')
default:
return forward(operation)
}
}
return forward(operation)
})
export default link
@nikitamarcius we posted workarounds above, take a look at observables
I am unable to update the token can anybody provided the working example
@Ramyapriya24 here is the code I am using.
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/link-context';
import AuthService from 'services/auth-service' // this is my implementation
const asyncAuthLink = setContext(async () => {
// this is an async call, it will be done before each request
const { token } = await AuthService.getCredentials();
return {
headers: {
authorization: token
},
};
},
);
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
export const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: asyncAuthLink.concat(httpLink),
});
@adrianolsk can you provide the service code wrote
import AuthService from 'services/auth-service' // this is my implementation const { token } = await AuthService.getCredentials();
when I am trying to import the service I am getting errors
That is my service, it just read the AsyncStorage from react-native, so after login I set the value there and before each request the code just grab the info and set in the header, you could do the same, or using localStorage if you are on the web.
Where are you storing the information you want to use?
you can just use this
//save the token after login or when it refreshes
localStorage.setItem('token', yourToken);
and use it
const asyncAuthLink = setContext(() => {
// grab token from localStorage
const token = localStorage.getItem('token');
return {
headers: {
authorization: token
},
};
},
);
@adrianolsk thanks for the explanation but I am using angular I am unable to import the service in grapqh.module.ts file I am getting errors when I am using the service
can anyone know how to use the service in module.ts file without using class and constructor
Thanks
I'm trying to use fromPromise
for async refresh the token.
Basically following the third box from this post
I'm successfully getting and storing the tokens, but neither of catch
or filter
or flatMap
gets called. I'm not sure how to debug this, so some suggestions will be helpful.
if (token && refreshToken) {
return fromPromise(
getNewToken(client)
.then(({ data: { refreshToken } }) => {
console.log("Promise data: ", refreshToken);
localStorage.setItem("token", refreshToken.token);
localStorage.setItem("refreshToken", refreshToken.refreshToken);
return refreshToken.token;
})
.catch((error) => {
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
console.log("Error after setting token: ", error);
return;
})
)
.filter((value) => {
console.log("In filter: ", value);
return Boolean(value);
})
.flatMap(() => {
console.log("In flat map");
// retry the request, returning the new observable
return forward(operation);
});
}
@adrianolsk: that approach seems to always refresh the token, even before it's expired, which in the case of some authentications services (e.g. Auth0's checkSession) will make an unnecessary Auth0 server roundtrip for every GraphQL request.
I'm trying to use
fromPromise
for async refresh the token. Basically following the third box from this postI'm successfully getting and storing the tokens, but neither of
catch
orfilter
orflatMap
gets called. I'm not sure how to debug this, so some suggestions will be helpful.if (token && refreshToken) { return fromPromise( getNewToken(client) .then(({ data: { refreshToken } }) => { console.log("Promise data: ", refreshToken); localStorage.setItem("token", refreshToken.token); localStorage.setItem("refreshToken", refreshToken.refreshToken); return refreshToken.token; }) .catch((error) => { // Handle token refresh errors e.g clear stored tokens, redirect to login, ... console.log("Error after setting token: ", error); return; }) ) .filter((value) => { console.log("In filter: ", value); return Boolean(value); }) .flatMap(() => { console.log("In flat map"); // retry the request, returning the new observable return forward(operation); }); }
I've found what was the cause of the error. Not seen in the code above, but I used a map
function to map each of the resulting errors. This caused onError
to return nothing and the observable wasn't subscribed to the operation for token renewal.
Pretty confusing and it took me so long to figure it out. Thanks to the author of the blog post for helping me out.
ERROR Error: Network error: Cannot read property 'length' of null
@WilsonLau0755, I had the same problem. Solved it by setting all null
headers to an empty string ''
.
Why is onError not just available to use with async await?
Issue Labels
Question
The precise scenario I'm trying to accomplish is mentioned here:
https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error#retrying-failed-requests
However, if a token is expired, an async
resfreshToken
should be called and "waited on", beforegetNewToken
can return a valid auth token. I think.My question is, how to do an async
resfreshToken
call. I've triedawait refreshToken()
(which resolves a promise when it's complete), but from my logged stack traces it seems that this messes with RxJS quite a lot. I'm an RxJS n00b, any help is greatly appreciated!