Apollo-server: Subscriptions onConnect return value is not appended to context

Created on 31 Aug 2018  路  8Comments  路  Source: apollographql/apollo-server


According to the docs for subscriptions in apollo-server-express 2.0, the return value of onConnect is appended to the context. However, in practice the return value of onConnect is ignored and only the context returned by the context method of ApolloServer config is available in resolvers.

For example, with this code:

const server = new ApolloServer({
    // These will be defined for both new or existing servers
    typeDefs,
    resolvers,
    context: async ({ req, connection }) => {
        if (connection) {
            // check connection for metadata
            return {
                foo: 'bar'
            };
        } else {
            // check from req
            const token = req.headers.authorization || "";

            return { token };
        }
    },
    subscriptions: {
        onConnect: (connectionParams, webSocket) => {
            console.log('Websocket CONNECTED');
            return {
                hello: 'world'
            }
        },
        onDisconnect: () => console.log('Websocket CONNECTED'),
    }
});

You would expect the context for websockets to be:
{foo: 'bar', hello: 'world'}
But in practice the context will be:
{foo: 'bar'}

I have created a basic reproduction of the issue here.

bug has-reproduction 馃К subscriptions

Most helpful comment

I was able to get hello object in the context if I manually add it to the context via connection.context. Like:

context: async ({ req, connection }) => {
        if (connection) {
            // check connection for metadata
            return {
                foo: 'bar',
                hello: connection.context.hello,  
            };
        } else {
            // check from req
            const token = req.headers.authorization || "";

            return { token };
        }
    },
    subscriptions: {
        onConnect: (connectionParams, webSocket) => {
            console.log('Websocket CONNECTED');
            return {
                hello: 'world'
            }
        },
        onDisconnect: () => console.log('Websocket CONNECTED'),
    }

Not sure if this is the correct approach though.

All 8 comments

Issue is still present in v2.1.0

I was able to get hello object in the context if I manually add it to the context via connection.context. Like:

context: async ({ req, connection }) => {
        if (connection) {
            // check connection for metadata
            return {
                foo: 'bar',
                hello: connection.context.hello,  
            };
        } else {
            // check from req
            const token = req.headers.authorization || "";

            return { token };
        }
    },
    subscriptions: {
        onConnect: (connectionParams, webSocket) => {
            console.log('Websocket CONNECTED');
            return {
                hello: 'world'
            }
        },
        onDisconnect: () => console.log('Websocket CONNECTED'),
    }

Not sure if this is the correct approach though.

Nice one @sabith-th. With that being the case, we can simply use the following as a workaround:

return Object.assign(
        {
          foo: "bar"
        },
        connection.context
      );

Just curious if anyone would find this approach helpful or have feedback =)

Right now I pass the token in two places:

First in the middleware, so that the token is always fresh on each call and allows for using withFilter with a user context that we know is current. NOTE: you need to have your lookup function inside of the applyMiddleware function, as it will be called on each request. If you declare it at the top of the file, it will be stale.

Second, in the connection params so that onConnect can see it and write it to context so that later onDisconnect can also see it, so that we can rely on those calls to set user online status.

// Client.js

import get from 'lodash/get';
import { WebSocketLink } from 'apollo-link-ws';

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    // Pass the token as a connection param so we can set the online status
    // on the onConnect callback in subscriptions
    connectionParams: {
      authToken: `Bearer ${get(JSON.parse(localStorage.getItem('user')), 'data.token')}`,
    },
    reconnect: true,
  },
});

// Pass the authToken as an option on each socket call so that the user
// data is never stale
const subscriptionMiddleware = {
  applyMiddleware: async (options, next) => {
    // eslint-disable-next-line no-param-reassign
    options.authToken = `Bearer ${get(JSON.parse(localStorage.getItem('user')), 'data.token')}`;
    next();
  },
};

// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware]);

export default wsLink;
// Server.js

const server = new ApolloServer({
  context: async ({ req, payload }) => {
    // Allow getting the auth from req or ws payload
    const authToken = get(req, 'headers.authorization') || get(payload, 'authToken');
    const user = await AuthServices.getUser(authToken);

    if (!user) {
      return { ...req, pubsub, models };
    }

    return {
      ...req,
      pubsub,
      user,
      models,
    };
  },
  formatError,
  playground: {
    endpoint: '/playground',
  },
  schema,
  subscriptions: {
    onConnect: async ({ authToken }) => {
      if (authToken) {
        const user = await AuthServices.getUser(authToken);
        if (user) {
          await user.setOnline();
          return {
            authToken,
          };
        }
      }
      return {};
    },
    onDisconnect: (params, socket) => {
      socket.initPromise.then(async ({ authToken }) => {
        if (authToken) {
          const user = await AuthServices.getUser(authToken);
          if (user) {
            await user.setOffline();
          }
        }
      });
    },
    path: '/graphql',
  },
});

@samalexander the only proper workaround I can see for right now is to fork the ApolloServer.installSubscriptionHandlers function and either make it not call this.context or make it merge the objects from onConnect and this.context.

@sabith-th actually it would be even better to do this:

        if (connection) {
            return {
                ...connection.context,
                foo: 'bar',
            };

What's the current status on this? This issue is open for over a year now and it's still not working

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dupski picture dupski  路  3Comments

mathroc picture mathroc  路  3Comments

bryanerayner picture bryanerayner  路  3Comments

espoal picture espoal  路  3Comments

veeramarni picture veeramarni  路  3Comments