apollo-client: It is not possible to pass Authorization header to WebSocket

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 

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 11
  • Comments: 17

Most upvoted comments

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.

@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… 😃

@michal-hudy Does this not work for you?

const wsLink = new WebSocketLink(
  new SubscriptionClient(WS_URL, {
    reconnect: true,
    timeout: 30000,
    connectionParams: {
      headers: {
        Authorization: "Bearer xxxxx"
      }
    }
  })
);

@pyankoff I’m using Hasura as well. how did you handle token change (refresh after expiration) with WebSocketLink?

The code example by @pyankoff did not work for me.

Instead, I had to supply the auth token in connectionParams.authorization rather than connectionParams.headers.Authorization:

const wsLink = new WebSocketLink({
  uri: WS_URL,
  options: {
    lazy: true,
    reconnect: true,
    connectionParams: async () => {
      const token = await getToken();
      return {
        // this works for me
        authorization: token ? `Bearer ${token}` : "",

        // this did not work for me
        //headers: {Authorization: token ? `Bearer ${token}` : ""},
      };
    },
  },
});

@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.

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.

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

@ARMATAV I’ve tried to implement code as mentioned, but it gives me type error for some of the wsClient functions. For example, when I try to call wsClient.sendMessage({}), it gives me Property 'sendMessage' is private and only accessible within class 'SubscriptionClient'. Is there any issue with typescript support?

If I recall, yes - // @ts-ignore that thing and it will work I believe

@georgyfarniev I think it depends on your server side implementation. I was using Hasura and didn’t have to handle it.