Meteor and Graphql Subscriptions normal DDP connection using same port as WebApp

I’ve setup a meteor server for subscriptions, and the socket seems to be connecting fine. However I found that the normal Meteor socket connection is Connection closed before receiving a handshake response. Is there a way to specify a different port for either the normal Meteor socket, or the one used by the meteor/webapp project?

Our use case is attempting to migrate to a graphql api interface incrementally.

Code below:

Server:

import { ApolloServer } from 'apollo-server-express';
import { WebApp } from 'meteor/webapp';
import { getUser } from 'meteor/apollo';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { makeExecutableSchema } from 'graphql-tools';
import { typeDefs } from './typedefs';
import { resolvers } from './resolvers';

const schema = makeExecutableSchema({ typeDefs, resolvers });

const server = new ApolloServer({
  schema,
  context: async ({ req }) => {
    const user = await getUser(req.headers.authorization);
    if (!user) throw new Error('must login');
    return {
      user,
    };
  },
});

server.applyMiddleware({
  app: WebApp.connectHandlers,
  path: '/graphql',
});

// eslint-disable-next-line
const subscriptionServer = new SubscriptionServer({
  execute,
  subscribe,
  schema,
  onConnect: async (connectionParams, webSocket) => {
    // if a meteor login token is passed to the connection params from the client,
    console.log('connection params!!!', connectionParams, webSocket);
    // add the current user to the subscription context
    // const subscriptionContext = connectionParams.meteorLoginToken
    //   ? await addCurrentUserToContext(context, connectionParams.meteorLoginToken)
    //   : context;

    return {};
  },
}, { server: WebApp.httpServer, path: '/subscriptions' });

WebApp.connectHandlers.use('/graphql', (req, res) => {
  if (req.method === 'GET') {
    res.end();
  }
});

Client:

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink, split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { MeteorAccountsLink } from 'meteor/apollo';
import { getMainDefinition } from 'apollo-utilities';

// "basic" Meteor network interface

// create a websocket uri based on your app absolute url (ROOT_URL), ex: ws://localhost:3000
const websocketUri = Meteor.absoluteUrl('subscriptions').replace(/^http/, 'ws');

console.log('websocketUri', websocketUri)

const wsLink = new WebSocketLink({
  uri: websocketUri,
  options: {
    reconnect: true,
    connectionParams: {
      authToken: 'aasdfs',
    },
  },
});

const httpLink = new HttpLink({
  uri: '/graphql',
});

// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
  // split based on operation type
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition'
      && definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

// enhance the interface with graphql subscriptions

// enjoy graphql subscriptions with Apollo Client
export const client = new ApolloClient({
  link: ApolloLink.from([
    new MeteorAccountsLink(),
    link,
  ]),
  cache: new InMemoryCache(),
});
1 Like

Just replace with 'subscriptions-transport-ws' with internal Apollo subscription installSubscriptionHandlers.

1 Like

Unfortunately that doesn’t work. server.installSubscriptionHandlers(WebApp.httpServer); will print the same error.

Haven’t been able to figure this one out myself. I’ve had to set DISABLE_WEBSOCKETS=1 to get it to work properly.

I have no idea what causes this. I have dug around in meteor source, and I suspect that the websocket server that Apollo creates gets removed by Meteor, based on the large comment here: https://github.com/meteor/meteor/blob/devel/packages/webapp/socket_file.js

We use something like this to use both the Meteor socket and the GraphQL subscription.

const subscriptionServer = SubscriptionServer.create(
  {
    schema,
    execute,
    subscribe,
  },
  {
    noServer: true,
  }
);

const wsServer = subscriptionServer["wsServer"];
const upgradeHandler = (req, socket, head) => {
  const pathname = urlParse(req.url).pathname;

  if (pathname === "/graphql") {
    wsServer.handleUpgrade(req, socket, head, (ws) => {
      wsServer.emit("connection", ws, req);
    });
  } else if (pathname.startsWith("/sockjs")) {
    // Don't do anything, this is meteor socket.
  } else {
    socket.close();
  }
};

WebApp.httpServer.on("upgrade", upgradeHandler);

WebApp.connectHandlers.use("/graphql", (req, res) => {
  if (req.method === "GET") {
    res.end();
  }
});

Hopefully this helps.