Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installSubscriptionHandlers can't coexist with other websockets on the same port #134

Closed
jedwards1211 opened this issue Nov 14, 2018 · 5 comments
Labels
project-apollo-server (legacy) LEGACY TAG DO NOT USE

Comments

@jedwards1211
Copy link

I'm trying to add a websocket for GraphQL subscriptions alongside a Meteor DDP connection.
Unfortunately, WebSocket.Server aborts the handshake for any upgrade request it receives for a different path than the one configured in my ApolloServer, so it breaks Meteor's DDP websocket.

It's possible to support two websockets on the same port/different paths by writing an upgrade listener that decides whether to forward the event to the WebSocket.Server for Apollo or ignore it and let Meteor DDP handle it. However, to do that I have to copy code out of ApolloServer.installSubscriptionHandlers (which could change in the future) so that I can pass my own WebSocket.Server instance to SubscriptionServer.create.

This is an unhappy workaround. Could we at least make it possible to pass our own WebSocket.Server to installSubscriptionHandlers? Or is there another way to make ApolloServer more flexible? I didn't have this problem with apollo-server-express v1 because it was a much less monolithic API.

I also asked for an option to ignore upgrade requests for other paths in WebSocket.Server: websockets/ws#1193 (comment). If that becomes a reality we would simply need a way to pass that option through.

@jedwards1211 jedwards1211 changed the title installSubscriptionHandlers can't coexist with other websockets installSubscriptionHandlers can't coexist with other websockets on the same port Nov 14, 2018
jedwards1211 referenced this issue in jedwards1211/apollo-server Nov 14, 2018
jedwards1211 referenced this issue in jedwards1211/apollo-server Nov 14, 2018
@jbaxleyiii jbaxleyiii transferred this issue from apollographql/apollo-server Jul 8, 2019
@taozhi8833998
Copy link

same issue

@ananthachetan
Copy link

When can we expect the fix for this? We are incrementally incorporating subscriptions into our setup which already has websockets using SocketIO and ran into this problem.

@lambdahands
Copy link

lambdahands commented Jan 8, 2020

Edit: I did more research on this issue, and it’s more complex than it seems on the surface - this library doesn’t have much control over it as the cause is coming from the ws package. See: websockets/ws#885 for a good explanation.

@ruohki
Copy link

ruohki commented Aug 29, 2020

running into this as well after fiddling with old solutions etc I came up with a working workaround that isn't too bad:
ignore most of the parts but pay attention to the bottom part where I create the SubscriptionServer

import { WebApp } from "meteor/webapp";
import { Meteor } from "meteor/meteor";

import * as http from 'http';
import * as net from 'net';
import * as WebSocket from 'ws'

import mongoose from "mongoose";
import url from "url";

import { ApolloServer } from "apollo-server-express";
import { ExpressContext } from "apollo-server-express/dist/ApolloServer";
import { buildSchema } from "type-graphql";
import { PubSub as ApolloPubSub } from "apollo-server-express";
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";

import { ObjectId } from "mongodb";
import { TypegooseMiddleware } from "./helper/typegooseMiddleware";
import { ObjectIdScalar } from "./helper/scalarObjectID";
import { getUser } from "./helper/getUser";
import { authChecker } from "./helper/authChecker";

import { UserResolver } from "./resolver/user.resolvers";
import { SubscriptionServer } from "subscriptions-transport-ws";
import { execute, subscribe } from "graphql";

export interface SubscriptionParams {
  authorization?: string
}

export interface GraphqlContext {
  userId?: string | null;
  user?: Meteor.User | null;
}

export const createMongoConnection = async (): Promise<typeof mongoose> => {
  const mongoUrl = process.env.MONGO_URL ?? "mongodb://localhost:3001/meteor";

  const mongo = await mongoose.connect(mongoUrl, {
    useUnifiedTopology: true,
    useNewUrlParser: true,
    useCreateIndex: true,
    useFindAndModify: false,
  });

  return mongo;
};

const createRedisPubSub = (redisUrl: string) => {
  return new RedisPubSub({
    publisher: new Redis(redisUrl),
    subscriber: new Redis(redisUrl),
  });
};

const createApolloPubSub = () => new ApolloPubSub();

export const createApolloServer = async () => {
  await createMongoConnection();

  const useRedis: string | undefined = process.env?.REDIS_URL ?? undefined;
  const pubSub = useRedis ? createRedisPubSub(process.env.REDIS_URL!) : createApolloPubSub();

  const schema = await buildSchema({
    resolvers: [UserResolver],
    pubSub,
    authChecker,
    scalarsMap: [{ type: ObjectId, scalar: ObjectIdScalar }],
    globalMiddlewares: [TypegooseMiddleware],
    validate: true,
  });

  const server = new ApolloServer({
    schema: schema,
    playground: true,
    tracing: true,
    context: async (context: ExpressContext): Promise<GraphqlContext> => {
      if (!context?.req?.headers) return {};
      if (!context?.req?.headers.authorization) return {};

      const { authorization } = context.req.headers;
      const user = await getUser(authorization);

      return user ? {
        user,
        userId: user?._id,
      } : {};
    },
  });

  const subscriptionServer = new SubscriptionServer({
    schema,
    execute,
    subscribe,
    onConnect: async (params: SubscriptionParams) => {
      if (params.authorization) {
        const user = await getUser(params.authorization);
        return user ? {
          user,
          userId: user?._id,
        } : {};
      }
    }
  }, {
    noServer: true,
  });

  server.applyMiddleware({
    //@ts-ignore this is compatible and stated in the docs of apollo-server-express to be compatible
    app: WebApp.connectHandlers,
    path: "/graphql",
  });

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

  //@ts-ignore we need to access the private field which typescript does not like
  const wsServer: WebSocket.Server = subscriptionServer.wsServer;
  const upgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
    if (!req.url) return;
    const pathname = url.parse(req.url).pathname;

    if (!pathname) return
    if (pathname.startsWith("/graphql")) {
      wsServer.handleUpgrade(req, socket, head, (ws) => {
        wsServer.emit("connection", ws, req);
      });
    }
  };
  WebApp.httpServer.on("upgrade", upgradeHandler);
};

hope this helps someone.
i was using Meteor 1.11 and meteor/apollo@4 here

repository using it: ruohki/meteor-graphql-mongoose-starter

@jerelmiller jerelmiller added the project-apollo-server (legacy) LEGACY TAG DO NOT USE label Apr 6, 2023
@trevor-scheer
Copy link
Member

trevor-scheer commented May 10, 2023

Subscriptions over websockets are no longer an Apollo Server concern as of v4, so any outstanding issues with using them would need to be taken up with the respective libraries (apparently ws) that you might be using.

Apollo Server remains compatible with subscriptions, but the functionality is now adjacent rather than internal.
https://www.apollographql.com/docs/apollo-server/data/subscriptions

@trevor-scheer trevor-scheer closed this as not planned Won't fix, can't repro, duplicate, stale May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
project-apollo-server (legacy) LEGACY TAG DO NOT USE
Projects
None yet
Development

No branches or pull requests

7 participants