Single WebSocket + multiple durable objects + prox...
# workers-help
z
Hello, I'm setting up a Worker/WebSocket + Durable Objects combo where I need to support a single WS connection from the client to multiple Durable Objects. So far, I set up the DO class that responds with a WS and a Worker that responds with another WS for the client, when the latter receives a message from the client it connects to the DO with
new WebSocket()
using the unique ID from the message. First I tried Service Bindings but that doesn't seem to support WebSockets yet. Then I tried this
new WebSocket()
connection but was getting 404 instead of 101 without hitting DO at all, after some trial and error and getting 1024 from a dummy fetch request, I found that CF doesn't allow connecting two Workers within the same account. So then I opened another account and set up the DO there. This worked but I would need to duplicate half my infrastructure to this second account in order to have all the Service Bindings and KV stores. This is less than ideal. Now, I'm trying to set up a simple WebSocket proxy on this 2nd account. ie.: Client -> Single WS (Account 1) -> Proxy (Account 2) -> DO WS (Account 1) -> Client My understanding is that this should just work by CF magic passing around the request but the WS keeps disconnecting right after connecting without any errors. I tested a simple fetch on the proxy and that worked. So I tried setting up a manual proxy where I listen for messages from the ws pair and pass it along to the DO, this seem to establish a persisting connection but nothing happens with the messaging. Not getting any errors just silently failing. I would appreciate any insights or suggestions.
Here is the custom proxy I have atm:
Copy code
export default {
  fetch: async (request: Request, env: Environment, context: ExecutionContext) => {
    const url = new URL(request.url)
    const doUrl = new URL('wss://' + env.DO_BASE + url.pathname)
    const upgradeHeader = request.headers.get('Upgrade')
    if (!upgradeHeader || upgradeHeader !== 'websocket') {
      return new Response('Expected Upgrade: websocket', { status: 426 })
    }

    const [client, server] = Object.values(new WebSocketPair())
    server.accept()

    const doWS = new WebSocket(pollenUrl)
    doWS.addEventListener('open', () => {
      console.log('open DO ws')
    })
    doWS.addEventListener('message', (e: MessageEvent) => {
      console.log('message DO ws', e.data)
      server.readyState === 1 && server.send(e.data)
    })
    doWS.addEventListener('close', () => {
      console.log('close DO ws')
    })
    doWS.addEventListener('error', error => {
      console.error('error DO ws', JSON.stringify(error))
    })

    server.addEventListener('open', event => {
      console.log('open proxy ws')
    })
    server.addEventListener('message', async e => {
      doWS.readyState === 1 && doWS.send(e.data)
    })
    server.addEventListener('error', error => {
      console.error('error proxy ws', JSON.stringify(error))
    })
    server.addEventListener('close', () => {
      console.log('close proxy ws')
    })

    return new Response(null, {
      status: 101,
      webSocket: client
    })
  }
} as ExportedHandler<Environment>
Even though it seems to connect it logs
'close proxy ws'
right after it logged
'open DO ws'
. Even though the proxy says it disconnected, the worker in Account 1 doesn't seem to say it disconnected or errored. None of the console logs in message handlers seem to trigger.
Got it working, after working through some miniflare build issues (due to how I set it up in package.json the proxy kept picking up values from the main toml,
[miniflare]
values were overwriten with
--wrangler-config
but not the others not). Now I have "single client facing WS" + "multiple DO" in a single Worker in Account 1 and the simple proxy Worker in Account 2. Here is the proxy code:
Copy code
export default {
  fetch: async (request: Request, env: Environment, context: ExecutionContext) => {
    const upgradeHeader = request.headers.get('Upgrade')
    if (!upgradeHeader || upgradeHeader !== 'websocket') {
      return new Response('Expected Upgrade: websocket', { status: 426 })
    }

    const sockets = new Map<string, WebSocket>()
    const [client, server] = Object.values(new WebSocketPair())
    server.accept()
    server.addEventListener('message', async event => {
      const doID = event.data.doID
      if (!sockets.has(doID)) {
        const url = new URL(request.url)
        const doUrl = new URL(env.DO_BASE + url.pathname)
        doUrl.protocol = doUrl.protocol.replace('http', 'ws')
        const doWS = new WebSocket(doUrl)
        sockets.set(doID, doWS)
        doWS.addEventListener('open', () => doWS.send(event.data))
        doWS.addEventListener('message', (event: MessageEvent) => {
          server.readyState === 1 && server.send(event.data)
        })
        doWS.addEventListener('close', () => sockets.delete(doID))
        doWS.addEventListener('error', error => console.error('Durable WS', JSON.stringify(error)))
      } else {
        sockets.get(doID)?.readyState === 1 && sockets.get(doID)?.send(event.data)
      }
    })
    server.addEventListener('error', error => console.error('Proxy WS', JSON.stringify(error)))

    return new Response(null, { status: 101, webSocket: client })
  }
} as ExportedHandler<Environment>
3 Views