I need to be able to add an authKey after the connection has already been made. What's the recommended way of doing this?
I'd really love to see an example for this as well. This is the most requested feature for subscription-transport-ws in the Graphcool community.
@Urigo @stubailo We might want to consider creating docs specifically for subscriptions. It seems to be a frequently requested feature.
A simple solution for this issue is to use a function instead of object for connectionParams, and the transport will call the function and use it's return value as the connection params.
So if the return value of the function changes - the connection params will change.
Note that you need to cause the transport to reconnect (using close(false)).
How would that look like when using subscription-transport-ws together with Apollo Client? Does the HOC withApollo expose the close function?
@marktani No, because withApollo exposes the instance of ApolloClient for GraphQL actions such as query, mutations and subscriptions.
For transport related actions, you need to use your SubscriptionClient instance directly.
I thought about this issue more, and I have some thoughts:
reinit(connectionParams) method, that closes the connection and creates it again. The problem here is that we are losing all active subscriptions, and the transport can't recover them because we do not persist the whole subscription operation (just the subscription key).reinit(connectionParams) but without closing the connection, just executing INIT message on the server, causing the transport server to call onConnecting again and rebuild the connection context.close(false) doesn't work for me, because I need the websocket to be immediately reconnecting, but doing close(false) will call tryReconnect which attempts to connect the websocket only after a backoff timeout. Meanwhile all new subscriptions are failing.
I'm currently using non public API to workaround this, but it would be great to have official immediate reconnect support.
// Close socket connection which will also unregister subscriptions on the server-side.
wsClient.close();
// Reconnect to the server.
wsClient.connect();
// Reregister all subscriptions.
Object.keys(wsClient.operations).forEach((id) => {
wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options);
});
@cvle , what do you think about exposing a new API for reinit() that do this logic?
I think having a nice API for that would be absolute fantastic. As mentioned before, reconnecting with different authentication information is a common use case for many.
See my solution: I add a token parameter to each operation:
const wsClient = new SubscriptionClient(`ws://localhost:5000/graphql`, {
reconnect: true
});
wsClient.use([
{
applyMiddleware(operationOptions, next) {
operationOptions['token'] = getToken();
next();
}
}
]);
On the server I parse the token in the onOperation callback, and add the user to the context.
@scf4 and @marktani do u think the problem is solved with @ribizli solution?
@mistic Seems more like a workaround. We shouldn't need to send the auth token with every operation. @cvle's solution is the best so far.
@helfer @mistic could someone from Apollo provide an example of the best way to do this, as things stand with subscriptions-transport-ws? Something like the HTTP example here would be much appreciated.
Currently I'm passing an authorization header when initialising the client:
const returnJWT = () => {
const token = localStorage.getItem("graphcoolToken");
return token ? `Bearer ${token}` : null;
};
const wsClient = new SubscriptionClient(SUBSCRIPTIONS_API_ENDPOINT, {
reconnect: true,
timeout: 20000,
connectionParams: {
Authorization: returnJWT()
}
});
I'm then using a middleware as follows:
wsClient.use([
{
applyMiddleware(operationOptions, next) {
operationOptions["Authorization"] = returnJWT();
next();
}
}
]);
Is this the correct way to add the token to operationOptions? My request is fine when the JWT is in place before initialising the client, but not when it's only added by the middleware.
Chiming in, as this is on Graphcool's end to support, which it currently does not. I created this feature request to further discuss this issue. Thanks! 🙏
@lewisblackwood did you have any luck finding a temporary workaround?
I used your example Lewis, but it's still not working for me... I'm still getting "Insufficient Permissions" when I have permissions enabled... however, it works fine when permissions are disabled. I have even hard coded in the server token, and it still doesn't work.
There seems to be some property naming inconsistency in the issue threads (listed below):
So is the correct property name "Authorization" or "authToken"? Either way, I've tested both, and both fail.
HERE ARE MY VERSIONS
react-native@latest
[email protected]
[email protected]
HERE IS MY ERROR
Unhandled GraphQL subscription error Array [
Object {
"code": 3008,
"locations": Array [
Object {
"column": 7,
"line": 4,
},
],
"message": "Insufficient Permissions",
"path": Array [
"User",
"node",
"id",
],
"requestId": "subscription:cj6...:1",
},
Object {
"code": 3008,
"locations": Array [
Object {
"column": 7,
"line": 5,
},
],
"message": "Insufficient Permissions",
"path": Array [
"User",
"node",
"type",
],
"requestId": "subscription:cj6...:1",
},
]
ISSUES I'VE REVIEWED
https://github.com/graphcool/feature-requests/issues/317
https://github.com/apollographql/subscriptions-transport-ws/issues/102
Hey @quadsurf - sorry I should have clarified on this issue.
I believe my example above is the correct way to use the client, but I don't think Graphcool have support for authorization middleware with WSS yet. You can track that feature here.
My temporary solution is to do a full reload after the user authenticates, which reinitialises the WS client (with the auth token from Graphcool now existing in local storage at this point).
Well I think the token should not be send on every operation but only on connection, that way token is check only once on server side.
Kind of a .resetConnection() doing @cvle workaround should be interesting.
Any updates on this?
Is there a way to use the new link for subscriptions? https://www.apollographql.com/docs/react/2.0-migration.html#middleware
I am using temporary solution for reconnect with new connectionParams
client.restartWebsocketConnection = () => {
if (wsLink) {
const { headers } = options;
if (typeof headers === 'function') {
wsLink.subscriptionClient.connectionParams = headers();
}
wsLink.subscriptionClient.tryReconnect();
}
};
wsLink is apollo-link-ws
client is apollo-client
headers is my own function for authorization
I will call client.restartWebsocketConnection() when use login/logout.
I am using it in the production and it is working fine.
For anybody that lands here after fighting with this for hours just wanting to handle login/logout situations (so just clean slate), the two approaches are:
wsClient.close(true) which forces closing the connection right awayreconnect: true as option when creating the client@ntziolis you are not able to change connectionParams with your aproach
You can, but its not well documented if at all. I only found it looking through the TS typings. Since you can declare connectionParams as a function there needs to be some event that trigger's retrieving the function. Which happens to be when a connection is created (other than http where it happens on every request)
So first you need to make sure to define connectionParams as a function, otherwise its only set during declaration and not every time a connection is created:
const wsClient = new SubscriptionClient("wss://url", {
reconnect: true,
timeout: 30000,
connectionParams: () => {
return { Authorization: `Bearer ${auth.getToken()}` }
}
})
So in order to force it to fetch a new token after login we need to call wsClient.close(true) right after we stored the new token.
The rest is handled by the reconnect: true setting which creates a new connection whenever there is non but one is required. So subsequent subscriptions will be using the new auth token.
UPDATE: I have this working in our live app now and this path seems to work well
Any updates on this?
@ntziolis I followed your advice and it is working fine for me. Thanks :)
I'm trying to refresh connectionParams by closing and opening a new connection.
It works. The connection is closed and a new one is made.
I also followed what @cvle said and I tried to push to the new connection the current operations.
It also works and I get data from server with the new subscription.
The only problem is that it does not trigger rxjs subscriptions client side that are listening it it. (I can see data coming from server and in subscription tab in Chrome though).
It's like trying to rebuild operations with sendMessage use a different space in memory than the rxjs subscription.
I know it's a bit of the topic. But do you guys have any idea how to avoid that?
Here is what I have.
returnJWT() {
const token = localStorage.getItem('token') || null;
return token ? { authToken: `Bearer ${token}` } : {};
}
connectWSLink() {
return new SubscriptionClient(environment.server.subscription, {
reconnect: true,
connectionParams: () => this.returnJWT()
});
}
private setupLink() {
// queries and mutations link (http)
const httpBatched = new BatchHttpLink({ uri: environment.server.graphql });
const wsClient: any = this.connectWSLink();
const websocket = new WebSocketLink(wsClient);
// event that leads to refreshing ws connection
this.appService.refreshTokens.subscribe(() => {
// Copy current operations
const operations: Operations = Object.assign({}, wsClient.operations);
// Close connection
wsClient.close(true);
// Open a new one
wsClient.connect();
// Push all current operations to the new connection
Object.keys(operations).forEach(id => {
wsClient.sendMessage(
id,
MessageTypes.GQL_START,
operations[id].options
);
});
});
...
Nevermind. I made it work thanks to this https://github.com/coralproject/talk/blob/32da779ac155af2547f411a81ce5d93aecdcdd5e/client/coral-framework/services/client.js#L7
this is how I do this with Apollo 2.0 links:
function createSubscriptionsLink(config, events) {
let headers
const subscriptions: any = new SubscriptionClient(config.get('eventsUrl'), {
reconnect: true,
reconnectionAttempts: 10,
timeout: 3000,
lazy: true,
connectionParams() {
return { headers }
}
});
events.topic('app:online')
.subscribe(() => {
// TODO: create PR to expose `tryReconnect` as public
subscriptions.tryReconnect()
})
events.topic('Session:new')
.subscribe(() => subscriptions.close(false, true))
events.topic('Session:destroyed')
.subscribe(() => {
subscriptions.close(true, true)
subscriptions.tryReconnect()
})
subscriptions.on('reconnected', () => {
events.publish('connection:restored')
})
subscriptions.on('disconnected', () => {
events.publish('connection:lost')
})
return new ApolloLink(operation => {
headers = operation.getContext().headers
return Observable.from(subscriptions.request(operation) as Observable<FetchResult>)
.map(response => {
operation.setContext({ response })
return response
})
})
}
Will this retain the server side subscriptions? Is there a grace period for when the server gc's the server side subscriptions, if so how long?
@ntziolis I don't know about server but client restores all subscriptions on re-connection. So, the time when server-side subscriptions will be GC-ed is not important, is it?
Doh ... old Meteor thinking kicking in on my end of heavy server side operations when closing / reopening subscriptions the wrong way
what's the difference between .close(true, true) and .close(false,false)?
.close(false, false) is treated like the connection dropped and a reconnect attempt is made. .close(true, true) is a forced close of the connection with no attempt to reconnect.
@ntziolis
Could you please elaborate more, i tried your advice and didn't get the result.
My problem i always geet the previous Token and not the one that i loged with, i need to refresh the page to get the new one. and i don't want to do this refresh page.
Passing an async function as connectionParams does not work:
const onConnect = async ({ Authorization }, webSocket, context) => {
if (!Authorization) throw new NoauthError();
const [token] = Authorization.split(' ').reverse();
try {
const valid = await validateToken(token);
return objOf('token', valid);
} catch (error) {
throw makeError(error);
}
};
const getStoredToken = () => {
const { accessToken } = JSON.parse(localStorage.getItem('session')) || {};
return accessToken;
};
const getCurrentToken = async () => {
const stored = getStoredToken();
return stored ? stored : await getUnauthenticatedToken();
};
connectionParamsconst connectionParams = async () => ({
Authorization: `Bearer ${await getStoredOrNewToken()}`
});
With this, server gets {} as connectionParams.
connectionParamsconst connectionParams = () => ({
Authorization: `Bearer ${getStoredToken()}`
});
I'll have to wait on the unauth token before initializing apollo client. It would be preferable if the constructor would accept a Promise of { Authorization } from the connectionParams function.
Also struggling to use a AsyncStorage for an authToken in connectionParams, any help on this?
We've been working around this using the following (pseudo-code):
const wsLink = new WebSocketLink({
uri: WSS_URI,
options: {
onError: (error) => {
// error.message has to match what the server returns.
if (error.message === 'Invalid authentication' && Auth.isAuthenticated()) {
// Reset the WS connection for it to carry the new JWT.
wsLink.subscriptionClient.close(false, false)
}
},
lazy: true,
reconnect: true,
connectionParams: () => ({
authorization: Auth.getToken(),
}),
},
})
Initially, we reset the connection every time the token changed, but that led to open connections dropping unnecessarily (since once the connection is opened, as long as it is maintained, the connection can live past token expiration – unless you have setup your WS server to close connections on token expiration, which is unlikely since that's quite overkill – WS connections are not that stable).
Awaiting for an explicit auth failure before re-configuration seems to be a sane option. We've been using this in production for about a month already without issues.
For those gathering the token asynchronously, the same approach might be used:
// This IIEF might be replaced by a stateful Link or any other custom class.
const wsLink = (() => {
let token
return new WebSocketLink({
uri: WSS_URI,
options: {
onError: async (error) => {
// error.message has to match what the server returns.
if (error.message === 'Invalid authentication' && Auth.isAuthenticated()) {
token = await Auth.getAsyncToken()
// Reset the WS connection for it to carry the new JWT.
wsLink.subscriptionClient.close(false, false)
}
},
lazy: true,
reconnect: true,
connectionParams: () => ({
authorization: token,
}),
},
})
})()
I implemented the solution given by @stefanmaric above but my onError was never actually called when my graphQL server throws an error in the context function due to the invalid token, however the regular apollo link error handler does get called. Perhaps something has changed in recent versions?
With the regular error link handler being called, you can just reset the wsLink from it, without creating a websocket-specific error handler.
class AuthorizedWsLink {
constructor (tokenProvider, wsSubscriptionsServer) {
this.wsSubscriptionsServer = wsSubscriptionsServer
this.tokenProvider = tokenProvider
tokenProvider.getToken().then(token => {
this.token = token
})
this.wsLink = new WebSocketLink({
uri: this.wsSubscriptionsServer,
options: {
lazy: true,
reconnect: true,
connectionParams: () => {
return ({
authorization: this.token
})
}
}
})
}
resetLink = async () => {
// get a new token
this.token = await this.tokenProvider.getToken()
// Reset the WS connection for it to carry the new JWT.
this.wsLink.subscriptionClient.close(false, false)
}
createLink = () => this.wsLink
}
and the link setup:
const authorizedWsLink = new AuthorizedWsLink(tokenProvider, wsSubscriptionsServer)
const wsLink = authorizedWsLink.createLink()
const errorHandler = onError(({ networkError, graphQLErrors, response, operation, forward }) => {
if (networkError && networkError.message === 'websocket-token-error') {
authorizedWsLink.resetLink()
}
})
I also need to update my authToken. To close and open the connection seems like a strange way to do this. I think tearing down the whole connection and re-establishing it should be considered "expensive" and only be used for error handling, not as part of normal communication flow. Yet every solution above seems to focus on re-connection.
So I'm wondering about the suggestion from @dotansimha above:
We can add reinit(connectionParams) but without closing the connection, just executing INIT message on the server, causing the transport server to call onConnecting again and rebuild the connection context.
This seems to me like the clean/unexpensive way to do it, was that option ever explored?
One year later, do we have updates on this or even a solution ?
You mean 3 years
It is very frustrating to repeatedly spend hours trying to get this work, unsuccessfully going through all the "solutions" posted in this thread.
This seems to be a very basic problem. It also seems that the maintainers are not interested in solving it.
😢
See https://github.com/apollographql/subscriptions-transport-ws/issues/777 for reasons why nothing ~is~ was happening
The only viable solution I found is to manually close the websocket client when user signs in or signs out. As WebSocketLink doesn't expose subscriptionClient you must provide it by yourself and pass it to the WebSocketLink constructor.
It's pretty overkill but it works...
I use Angular for all my projets, if someone is interested here is my code.
src/graphql.module.ts
import { NgModule } from '@angular/core';
import { ApolloClientOptions, InMemoryCache, split } from '@apollo/client/core';
import { setContext } from '@apollo/client/link/context';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { OperationDefinitionNode } from 'graphql';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { environment } from '../environments/environment';
import { TokenService } from './core/token.service';
export function createSubscriptionClient(token: TokenService): SubscriptionClient {
return new SubscriptionClient(environment.api.ws, {
reconnect: true,
connectionParams: () => ({
Authorization: token.get() ? `Bearer ${token.get()}` : '',
}),
});
}
export function createApollo(
httpLink: HttpLink,
token: TokenService,
wsClient: SubscriptionClient
): ApolloClientOptions<any> {
const http = httpLink.create({ uri: environment.api.http });
const ws = new WebSocketLink(wsClient);
const authLink = setContext(() => ({
headers: {
Authorization: token.get() ? `Bearer ${token.get()}` : '',
},
}));
const splitLink = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query) as OperationDefinitionNode;
return kind === 'OperationDefinition' && operation === 'subscription';
},
ws,
http,
);
return {
link: authLink.concat(splitLink),
cache: new InMemoryCache()
};
}
@NgModule({
providers: [
{
provide: SubscriptionClient,
useFactory: createSubscriptionClient,
deps: [TokenService],
},
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink, TokenService, SubscriptionClient],
},
],
})
export class GraphQLModule {}
src/sign/sign.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Apollo } from 'apollo-angular';
import { Observable } from 'rxjs';
import { pluck, tap } from 'rxjs/operators';
import { LoginGQL, LoginMutation } from 'src/graphql';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { TokenService } from '../core/token.service';
@Injectable({
providedIn: 'root',
})
export class SignService {
constructor(
private token: TokenService,
private router: Router,
private wsClient: SubscriptionClient,
private apollo: Apollo,
private login: LoginGQL,
) {}
signIn(email: string, password: string): Observable<LoginMutation> {
return this.login.mutate({ email, password }).pipe(
pluck('data'),
tap(({ login }) => {
this.token.set(login.token);
this.wsClient.close();
}),
);
}
signInAndRemember(email: string, password: string): Observable<LoginMutation> {
return this.signIn(email, password).pipe(tap(({ login }) => this.token.persist(login.token)));
}
logout(): void {
this.token.clear();
this.wsClient.close();
this.apollo.client.resetStore();
this.router.navigateByUrl('/sign-in');
}
}
The solution that works for me and seems simpler than above. I subscribe to my own onAuthTokenReceived event and use that to throw away the entire Apollo client make a new one.
````jsx
const Site = ({ children }) => {
const [apolloClient, setApolloClient] = useState(createApolloClient());
const handleAuthTokenReceived = () => {
setApolloClient(createApolloClient());
};
return (
);
};
// somewhere in createApolloClient()...
const wsLink = new WebSocketLink({
uri: ${origin}${apiEndpointPath},
options: {
reconnect: true,
connectionParams: {
headers: { // This is the shape my API (Hasura) wants
...getAuthHeaders(), // On the first pass this comes up empty handed
},
},
},
});
````
This way the site happily renders on the first pass but subscriptions only get public stuff because <AuthProvider> hasn't yet exchanged tokens etc. (Auth0 in this case doing a silent auth or waiting for the user to login). When <AuthProvider> receives a token it dispatches the event causing the whole tree to re-render with socket connecting with a token and subscriptions coming alive. This solves the problem of manually re-subscribing to everything in the tree and doesn't use any private APIs.
If you have React, you can temporarily use this solution
withApollo.js
-
import {ApolloProvider, ApolloClient} from "@apollo/client";
function createApolloClient(options) {
// create client the way you want
return new ApolloClient()
}
export const SetApolloOptionsContext = React.createContext()
export function withApollo(Component, firstOptions = {}) {
function WithApollo(props) {
const [client, setClient] = useState(() => {
return createApolloClient(firstOptions)
})
const setApolloOptions = useCallback((newOptions) => {
if (client) client.stop() // destroy prev client
setClient(createApolloClient(newOptions)) // create new client
}, [client])
return (
<SetApolloOptionsContext.Provider value={setApolloOptions}>
<ApolloProvider client={client}>
{<Component {...props}/>}
</ApolloProvider>
</SetApolloOptionsContext.Provider>
)
}
WithApollo.displayName = `WithApollo(${
Component.displayName || Component.name || 'Component'
})`
return WithApollo
}
Naruto.js
-
import withApollo, {SetApolloOptionsContext} from './withApollo'
function Naruto() {
const setApolloOptions = useContext(SetApolloOptionsContext)
function onClick() {
// update options
setApolloOptions({uri: Math.random()})
}
return (
<button onClick={onClick}>
change options
</button>
)
}
export default withApollo(Naruto, {uri: '/graphql'})
Thanks for your extremely helpful Angular example, @MikaStark. I was able to work-around the issue in our [email protected] / @apollo/[email protected] project with a simplified version of your approach. Instead of manually providing SubscriptionClient, I simply instantiated it directly from an 'subscriptions-transport-ws' import when configuring ApolloClient, adding the instance as a private class property to our existing ApolloClient service class to allow calling its close method when needed:
import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { ApolloError, ApolloLink, InMemoryCache, Operation, split } from '@apollo/client/core';
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { onError } from '@apollo/client/link/error';
import { getMainDefinition } from '@apollo/client/utilities';
import { OperationDefinitionNode } from 'graphql';
import { environment } from '../../environments/environment';
import { GQLResolversService } from './gql-resolvers.service';
import { MutationMonitorLinkService } from './mutation-monitor-link.service';
import { AuthenticationLinkService } from './authentication-link.service';
import { ErrorLinkService } from './error-link.service';
import { MutationInProgressListRead_Query } from '../../gql-operations/client/mutation-in-progress-list.graphql';
import { ViewStateParameters } from '../view-state/view-state-parameters.model';
import { ViewStateParametersRead_Query } from '../../gql-operations/client/view-state-parameters-read.graphql';
import { AuthenticationService } from '../authentication.service';
@Injectable({
providedIn: 'root'
})
export class GQLClientService {
private webSocketClient: SubscriptionClient;
constructor(private apollo: Apollo,
private httpLink: HttpLink,
private authenticationLinkService: AuthenticationLinkService,
private errorLinkService: ErrorLinkService,
private resolversService: GQLResolversService,
private authenticationService: AuthenticationService) {
this.start();
}
/** configures Apollo client to access GraphQL Application Programming Interface */
private start(): void {
const httpLink = this.httpLink.create({uri: `${environment.serverUrlBrowser}${environment.graphQLpath}`});
this.webSocketClient = new SubscriptionClient(environment.webSocketURL, {
lazy: true,
reconnect: true,
connectionParams: () => { return {authenticationToken: this.authenticationService.getSignInToken()}; }
});
const webSocketLink = new WebSocketLink(this.webSocketClient);
this.apollo.create({
cache: GQLClientService.initializeCache(new InMemoryCache()),
defaultOptions: {
// …
},
link: ApolloLink.from([
new ApolloLink(this.authenticationLinkService.link.bind(this.authenticationLinkService)),
onError(this.errorLinkService.onError.bind(this.errorLinkService)),
// …
split(({query}: Operation) => {
const {kind, operation}: OperationDefinitionNode = getMainDefinition(query) as OperationDefinitionNode;
return kind === 'OperationDefinition' && operation === 'subscription';
},
webSocketLink,
httpLink
)
]),
resolvers: this.resolversService.getResolvers(),
}
);
}
public closeWebSocket(): void {
this.webSocketClient.close(true);
}
// …
}
We then call GQLClientService.closeWebSocket() inside tap from our UserSignInMutation and UserSignOutMutation handlers.
2021 is here and no clean solution for this near-4-years issue... The only viable solution is to deal with the SubscriptionClient itself outside the WebSocketLink.
In that case, why don't just mark WebSocketLink.subscriptionClient as public instead of private ? And if the answer is "No, it's ugly and anti-pattern" then why don't add a close public method on WebSocketLink that will call subscriptionClient.close() ?
Also it will be great if Client.stop() could call WebSocketLink.subscriptionClient.close() ? This two solutions will solve everyone problems and don't seems hard to implement
2021 is here and no clean solution for this near-4-years issue... The only viable solution is to deal with the
SubscriptionClientitself outside theWebSocketLink.
In that case, why don't just markWebSocketLink.subscriptionClientas _public_ instead of _private_ ? And if the answer is "No, it's ugly and anti-pattern" then why don't add a _close_ public method onWebSocketLinkthat will callsubscriptionClient.close()?Also it will be great if Client.stop() could call WebSocketLink.subscriptionClient.close() ? This two solutions will solve everyone problems and don't seems hard to implement
thinking the same thing, using a private method feels horrible but there is no other way
@MikaStark The approach you proposed is the only viable one I could came across to successfully reset the connection params. But when you say that it's pretty overkill, does that mean that it could have an impact on the performance of the app or memory usage?
The only viable solution I found is to manually close the websocket client when user signs in or signs out. As
WebSocketLinkdoesn't exposesubscriptionClientyou must provide it by yourself and pass it to theWebSocketLinkconstructor.
It's pretty overkill but it works...
Btwn, I run some performance tests on chrome by logging in and out some users, and I didn't notice a real resource degradation. But I don' t know if this test is enough to say that this solution is completely safe.
@StriderRanger I said it’s overkill because you are forced to inject the SubscriptionClient yourself and manage it outside GraphQLModule (in our case SignService).
It works perfectly but in my opinion it’s a lot of codes for such little purpose. But it works and for now I use it in my every projects.
I think it may be a good idea to push this example in a markdown file or something as it might help people struggling with this issue without reaching this page.
Could we also have the ability to change the url to connect to as well?
The SubscriptionClient.url is private. If that wasn't the case then you could do
wsClient.url = '<url>';
wsClient.close(false);
i ran into this scenario (late april 2021) while working on a react-native project.. went ahead and created a general-use example of what I did to make this work for anyone who lands here and is struggling on this. hope it helps.
for context, noticed this issue after logging into a user account, logging out, logging back in with a different user account. saw subscriptions related to the previous user were still around.
currently using:
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { RetryLink } from '@apollo/client/link/retry';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from '@apollo/client/link/ws';
const subscriptionClient = new SubscriptionClient('wss://www.example.com/graphql', {
lazy: true,
reconnect: true,
connectionParams: () => {
const token = yourGetAccessTokenFunctionHere();
return {
headers: {
Authorization: token ? `Bearer ${token}` : '',
},
};
},
});
const webSocketLink = new WebSocketLink(subscriptionClient);
const httpLink = new HttpLink({
credentials: 'include',
uri: 'https://www.example.com/graphql',
});
const link = new RetryLink({ attempts: { max: Infinity } }).split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
webSocketLink,
httpLink,
);
class ApolloClientWS extends ApolloClient {
constructor(props) {
super(props);
this.close = this.close.bind(this);
}
close() {
return subscriptionClient.close();
}
}
const client = new ApolloClientWS({
link,
cache: new InMemoryCache(),
shouldBatch: true,
});
export default client;
import React from 'react';
import { withApollo } from '@apollo/client/react/hoc';
function LogoutButton(props) {
return <button onClick={props.client.close}>Logout</button>;
}
export default withApollo(LogoutButton);
Most helpful comment
close(false)doesn't work for me, because I need the websocket to be immediately reconnecting, but doingclose(false)will calltryReconnectwhich attempts to connect the websocket only after abackofftimeout. Meanwhile all new subscriptions are failing.I'm currently using non public API to workaround this, but it would be great to have official immediate reconnect support.