Intended outcome:
Since Apollo Client 2 it is not possible to pass custom HTTP Header to WebSocket connection. In Apollo Client 1 it was possible by Middleware
, but since version 2 it is not. I tried with additional link concat
, or by applying Middleware
to subscriptionClient
.
Sample with authorized link:
const httpLink = createHttpLink({ uri: `https://ws.server.local/graphql` });
const wsLink = new WebSocketLink({
uri: `wss://ws.server.local/graphql`,
options: {
reconnect: true,
},
});
const middlewareLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
authorization: getBearerToken() || null,
},
});
return forward(operation);
});
const authorizedLink = middlewareLink.concat(wsLink);
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
authorizedLink,
httpLink,
);
const client = new ApolloClient({
link: link,
...
});
Sample with subscriptionClient Middleware
const httpLink = createHttpLink({ uri: `https://ws.server.local/graphql` });
const wsLink = new WebSocketLink({
uri: `wss://ws.server.local/graphql`,
options: {
reconnect: true,
},
});
const subscriptionMiddleware = {
applyMiddleware(options, next) {
console.log(options);
options.setContext({
headers: {
authorization: getBearerToken() || null,
},
});
next();
},
};
wsLink.subscriptionClient.use([subscriptionMiddleware]);
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: link,
...
});
Versions
System:
OS: macOS High Sierra 10.13.6
Binaries:
Node: 9.5.0 - /usr/local/bin/node
npm: 5.6.0 - /usr/local/bin/npm
Browsers:
Chrome: 69.0.3497.100
Firefox: 60.0.2
Safari: 12.0
npmPackages:
apollo-boost: ^0.1.3 => 0.1.15
apollo-client: ^2.4.2 => 2.4.2
apollo-link-ws: ^1.0.9 => 1.0.9
react-apollo: ^2.0.4 => 2.1.11
@michal-hudy Does this not work for you?
```javascript
const wsLink = new WebSocketLink(
new SubscriptionClient(WS_URL, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
Authorization: "Bearer xxxxx"
}
}
})
);
It won't work as WebSocket API in browsers doesn't support setting custom headers, apart from the value of Sec-Websocket-Protocol
header.
https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/41521871
Please see @pkosiec comment.
Struggled with adding async function for connection params, was getting start received before the connection is initialised
error. Fixed it by adding lazy: true
to connection options:
const wsLink = new WebSocketLink({
uri: WS_URL,
options: {
lazy: true,
reconnect: true,
connectionParams: async () => {
const token = await getToken();
return {
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
}
},
},
})
Just in case someone having the same issue.
@pyankoff how can I read this header on server side?
@georgyfarniev I think it depends on your server side implementation. I was using Hasura and didn't have to handle it.
@pyankoff I'm using Hasura as well. how did you handle token change (refresh after expiration) with WebSocketLink?
Hey, @pyankoff can you please share how you were able to re-authenticate the user once the token has expired.
I am able to get new accessToken
using refreshToken
but I am failing to pass the new accessToken to client.
I have read through few examples and all direct towards using operation.setContext
, which I was unable to implement while using WebSocketLink
.
Thanks.
@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)
@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)
Hey, @lroy83 this what exactly I meant, thank you for sharing the code.
@lroy83 Oh man, I spent a long time searching for an example like what you posted. Thank you for sharing!
@dreamer01 If i understand currently your question is like mine.
If so then on WebSocketLink you can only pass tokens on connect.
if your token expires and you get a new one, you need to reconnect with the new tokens.
here is an example.
And if that wasn't your question, then maybe it will help someone else.. :)
Finally an actual code example for how to refresh tokens with websocket properly! Pasting it here just because the past 2 examples I've been linked 404'd.
import { ApolloClient } from 'apollo-client'
import { split, from } from 'apollo-link'
import { createUploadLink } from 'apollo-upload-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import MessageTypes from 'subscriptions-transport-ws/dist/message-types'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
import { setContext } from 'apollo-link-context'
import { withClientState } from 'apollo-link-state'
// Create the apollo client
export function createApolloClient ({
// Client ID if using multiple Clients
clientId = 'defaultClient',
// URL to the HTTP API
httpEndpoint,
// Url to the Websocket API
wsEndpoint = null,
// Token used in localstorage
tokenName = 'apollo-token',
// Enable this if you use Query persisting with Apollo Engine
persisting = false,
// Is currently Server-Side Rendering or not
ssr = false,
// Only use Websocket for all requests (including queries and mutations)
websocketsOnly = false,
// Custom starting link.
// If you want to replace the default HttpLink, set `defaultHttpLink` to false
link = null,
// If true, add the default HttpLink.
// Disable it if you want to replace it with a terminating link using `link` option.
defaultHttpLink = true,
// Options for the default HttpLink
httpLinkOptions = {},
// Custom Apollo cache implementation (default is apollo-cache-inmemory)
cache = null,
// Options for the default cache
inMemoryCacheOptions = {},
// Additional Apollo client options
apollo = {},
// apollo-link-state options
clientState = null,
// Function returning Authorization header token
getAuth = defaultGetAuth,
// Local Schema
typeDefs = undefined,
// Local Resolvers
resolvers = undefined,
// Hook called when you should write local state in the cache
onCacheInit = undefined,
}) {
let wsClient, authLink, stateLink
const disableHttp = websocketsOnly && !ssr && wsEndpoint
// Apollo cache
if (!cache) {
cache = new InMemoryCache(inMemoryCacheOptions)
}
if (!disableHttp) {
const httpLink = createUploadLink({
uri: httpEndpoint,
...httpLinkOptions,
})
if (!link) {
link = httpLink
} else if (defaultHttpLink) {
link = from([link, httpLink])
}
// HTTP Auth header injection
authLink = setContext((_, { headers }) => {
const authorization = getAuth(tokenName)
const authorizationHeader = authorization ? { authorization } : {}
return {
headers: {
...headers,
...authorizationHeader,
},
}
})
// Concat all the http link parts
link = authLink.concat(link)
}
// On the server, we don't want WebSockets and Upload links
if (!ssr) {
// If on the client, recover the injected state
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-underscore-dangle
const state = window.__APOLLO_STATE__
if (state && state[clientId]) {
// Restore state
cache.restore(state[clientId])
}
}
if (!disableHttp) {
let persistingOpts = {}
if (typeof persisting === 'object' && persisting != null) {
persistingOpts = persisting
persisting = true
}
if (persisting === true) {
link = createPersistedQueryLink(persistingOpts).concat(link)
}
}
// Web socket
if (wsEndpoint) {
wsClient = new SubscriptionClient(wsEndpoint, {
reconnect: true,
connectionParams: () => {
const authorization = getAuth(tokenName)
return authorization ? { authorization, headers: { authorization } } : {}
},
})
// Create the subscription websocket link
const wsLink = new WebSocketLink(wsClient)
if (disableHttp) {
link = wsLink
} else {
link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' &&
operation === 'subscription'
},
wsLink,
link
)
}
}
}
if (clientState) {
console.warn(`clientState is deprecated, see https://vue-cli-plugin-apollo.netlify.com/guide/client-state.html`)
stateLink = withClientState({
cache,
...clientState,
})
link = from([stateLink, link])
}
const apolloClient = new ApolloClient({
link,
cache,
// Additional options
...(ssr ? {
// Set this on the server to optimize queries when SSR
ssrMode: true,
} : {
// This will temporary disable query force-fetching
ssrForceFetchDelay: 100,
// Apollo devtools
connectToDevTools: process.env.NODE_ENV !== 'production',
}),
typeDefs,
resolvers,
...apollo,
})
// Re-write the client state defaults on cache reset
if (stateLink) {
apolloClient.onResetStore(stateLink.writeDefaults)
}
if (onCacheInit) {
onCacheInit(cache)
apolloClient.onResetStore(() => onCacheInit(cache))
}
return {
apolloClient,
wsClient,
stateLink,
}
}
export function restartWebsockets (wsClient) {
// Copy current operations
const 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
)
})
}
function defaultGetAuth (tokenName) {
if (typeof window !== 'undefined') {
// get the authentication token from local storage if it exists
const token = window.localStorage.getItem(tokenName)
// return the headers to the context so httpLink can read them
return token ? `Bearer ${token}` : ''
}
}
It took literally forever to find your comment with this example.
Most helpful comment
Struggled with adding async function for connection params, was getting
start received before the connection is initialised
error. Fixed it by addinglazy: true
to connection options:Just in case someone having the same issue.