feat(subscriptions): enable passing WebSocket.Server instead of HttpServer.#2314
Conversation
cd9b0c5 to
fb56543
Compare
…stead of HttpServer
For people who need to have multiple WebSocket servers sharing a single HTTP(S) server, the `ws` docs recommend [creating multiple instances of `new WebSocket.Server({ noServer: true })`](https://github.com/websockets/ws#multiple-servers-sharing-a-single-https-server). This PR makes `installSubscriptionHandlers` accept a `WebSocket.Server` instance (in addition to accepting an http server) to support the multiple WebSocket servers use case.
Example:
```js
const graphQLWebSocket = new WebSocket.Server({
noServer: true,
})
apolloServer.installSubscriptionHandlers(graphQLWebSocket)
const symmetryWebSocket = new WebSocket.Server({
noServer: true,
})
symmetryWebSocket.on('connection', socket => new SymmetryServer({socket}))
/**
* This is a workaround. When WebSocket.Server handles an upgrade request,
* it will abort the handshake if the request path doesn't match -- meaning it
* will break Meteor's DDP web socket. So we only want the WebSocket.Server
* for GraphQL to receive the upgrade event if it's not for Meteor.
*/
httpServer.on('upgrade', (request: IncomingMessage, socket: net$Socket, head: any) => {
if (request.url.startsWith(SYMMETRY_BROWSER_PATH)) {
symmetryWebSocket.handleUpgrade(request, socket, head, (ws: WebSocket) => {
symmetryWebSocket.emit('connection', ws)
})
} else if (request.url.startsWith(GRAPHQL_PATH)) {
graphQLWebSocket.handleUpgrade(request, socket, head, (ws: WebSocket) => {
graphQLWebSocket.emit('connection', ws)
})
} else {
socket.destroy()
}
})
```
fb56543 to
11ec6f0
Compare
|
@abernix can we get this merged? |
|
I fixed the conflict in the changelog, but I hope you can merge this soon so I don't have to keep merging conflicts over and over again |
|
@abernix this would be great to have |
|
@abernix please include this in v3 |
1 similar comment
|
@abernix please include this in v3 |
|
I strongly support this pull request as I find very difficult to patch it on a fork. Sadly we use apollo-server-express, which depends on apollo-server-core. So I would have to fork multiple projects in order to use this patch :( @martijnwalraven @trevor-scheer When do you plan the next release of apollo-server-core? |
|
Thank you so much @jedwards1211 !!! I did not exactly use your solution as my server is instantiated by a sub-library. But it gave me the idea of monkey-patching the already instantiated Object! And it works perfectly! That will do it until they release the patch :) |
|
You're welcome! Yeah what I did with |
|
Please merge this, +1 needed here. |
|
@jedwards1211 how is the work around meant to be used? Currently have something like this, cut down for example sake.import stoppable from 'stoppable';
import express from 'express';
import http from 'http';
import { ApolloServer } from 'apollo-server-express';
import { graphql } from './graphql';
const app = express();
const graphApp = new ApolloServer(graphql);
graphApp.applyMiddleware({app});
const httpServer = http.createServer(app);
const stoppableServer = stoppable(httpServer);
graphApp.installSubscriptionHandlers(stoppableServer); |
|
@OmgImAlexis basically just monkeypatch the It's not clear from your example that you need this -- this workaround/PR is intended for cases where you need multiple separate websocket handlers. Do you need multiple websocket handlers? |
|
@martijnwalraven @abernix will this PR ever get merged? Will we ever get to stop monkeypatching? |
|
@jedwards1211 sorry to be a bother but how exactly do I do that? |
import express from 'express'
import http from 'http'
import WebSocket from 'ws'
import { ApolloServer } from 'apollo-server-express'
const app = express()
const httpServer = http.createServer(app)
const graphQLWebSocket = new WebSocket.Server({ noServer: true })
const otherWebSocket = new WebSocket.Server({ noServer: true })
const GRAPHQL_PATH = '/graphql'
const OTHER_WEB_SOCKET_PATH = '/other'
httpServer.on('upgrade', (request, socket, head) => {
const { url } = request
if (url.startsWith(GRAPHQL_PATH)) {
graphQLWebSocket.handleUpgrade(request, socket, head,ws => {
graphQLWebSocket.emit('connection', ws)
})
} else if (url.startsWith(OTHER_WEB_SOCKET_PATH)) {
otherWebsocket.handleUpgrade(request, socket, head, ws => {
otherWebsocket.emit('connection', ws)
})
} else {
socket.destroy()
}
})
const apolloServer = new ApolloServer({
path: GRAPHQL_PATH,
// you'll need to provide/flesh out the following
schema,
context: ({req, connection}) => ...,
subscriptions: {
path: GRAPHQL_PATH,
onConnect: (connectionParams, websocket) => ...,
},
})
apolloServer.installSubscriptionHandlers = <the big function from my gist>
apolloServer.applyMiddleware({ app, path: GRAPHQL_PATH })
apolloServer.installSubscriptionHandlers(graphQLWebSocket)
otherWebSocket.on('connection', ...)
httpServer.listen(4000) |
|
This PR is opened for a while and the solution seems to be elegant to have own websocket configuration. @abernix @martijnwalraven can this PR be reviewed quickly and merge it? |
|
I use multiple graphql schemes on a single http server. I would like to be able to run several web socket servers. I would like this PR to be merged. |
This comment has been minimized.
This comment has been minimized.
WebSocket.Server instead of HttpServer.
abernix
left a comment
There was a problem hiding this comment.
This looks non-controversial enough to bring in without being a breaking change. Sorry this has sat for so long! We're trying to make our way through the backlog of things to be addressed, but subscriptions has often taken a backseat. We're working to consider it more carefully going forward, but do bear with us while we work toward eventually making it a more first-class citizen (i.e. actually benefit from the request pipeline and what it brings in terms of plugin support, caching, observability, etc.)
|
@abernix thanks a bunch! Even https://graphql.org/learn/ still doesn't even mention subscriptions AFAIK, so subs are kind of the second-class citizen of the whole ecosystem. Food for thought: in one of my projects I've started a pattern of using matching query and subscription fields implement an "observable" queries. For it to work I have to do a |
|
I've run into the same problem - needing to run two WS on the same http server, but for some reason I can't get the subscriptions to work. Work is based off of the examples for The timesync feature works great, it enters the With a single websocket the subscriptions work great: but once we add another websocket it doesn't. Does anyone have any idea what might be going on or how to debug this? |
|
@okeren-cx you might be having issue #4198, check if there are multiple versions of |
|
@jedwards1211 thanks! it was indeed the problem. I ended up monkey patching in the same way you showed here and it worked. |
|
@jedwards1211 and others I had this issue and the issue in #4198. TL;DR until some changes are merged or transport is changed the monkey patch is the way to make it work, this merge request doesn't solve #4198. The version by @jedwards1211 is in typescript, here is a working javascript equivalent. Thanks to all people that did a lot of the digging before 👍 https://gist.github.com/timkock/b38ae86cf5634c63dc482c7fc1c66be1 |
For people who need to have multiple WebSocket servers sharing a single HTTP(S) server, the
wsdocs recommend creating multiple instances ofnew WebSocket.Server({ noServer: true }). This PR makesinstallSubscriptionHandlersaccept aWebSocket.Serverinstance (in addition to accepting an http server) to support the multiple WebSocket servers use case.Real-life example from my code:
TODO:
installSubscriptionHandlersis not currently tested anywhere)