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

WebSockets #1153

Closed
hanumanjiblog opened this issue Jun 2, 2023 · 33 comments
Closed

WebSockets #1153

hanumanjiblog opened this issue Jun 2, 2023 · 33 comments

Comments

@hanumanjiblog
Copy link

Please implement WebSockets feature in honojs.

https://developers.cloudflare.com/workers/runtime-apis/websockets/

@zisra
Copy link

zisra commented Jun 7, 2023

Not just cloudflare, but also Node.js and deno

@yusukebe
Copy link
Member

yusukebe commented Jun 9, 2023

You can use WebSockets with the current version of Hono on Deno.

Refer: https://github.com/orgs/honojs/discussions/755

It can also be supported on Cloudflare Workers.

More info: https://developers.cloudflare.com/workers/learning/using-websockets/

Supporting more WebSocket features is outside the scope of Hono.

For Node.js, you can't use WebSockets with the current Node.js Adapter and there are no plans to implement it. But, if you want this feature, you might be able to write the PR yourself.

@JustJoostNL
Copy link

Is there an example with Hono and CloudFlare workers too? I looked into the documentation of CloudFlare, but couldn't get it working with Hono.

@yusukebe
Copy link
Member

yusukebe commented Oct 17, 2023

Hi @JustJoostNL !

I've written an example. Try this!

// websocket.ts
import type { WebSocket as CFWebSocket } from '@cloudflare/workers-types'
import { Hono } from 'hono'
import { html } from 'hono/html'

const app = new Hono()

app.get('/', (c) => {
  return c.html(
    html`<html>
      <body>
        <script>
          const hostname = window.location.host
          const protocol = document.location.protocol === 'http:' ? 'ws://' : 'wss://'
          const ws = new WebSocket(protocol + hostname + '/websocket')
          ws.addEventListener('message', ({ data }) => {
            console.log(data)
          })
        </script>
        <h1>WebSocket</h1>
      </body>
    </html>`
  )
})

app.get('/websocket', (c) => {
  const upgradeHeader = c.req.header('Upgrade')
  if (upgradeHeader !== 'websocket') {
    return c.text('Expected websocket', 400)
  }

  const webSocketPair = new WebSocketPair()
  const client = webSocketPair[0]
  const server = webSocketPair[1] as unknown as CFWebSocket
  server.accept()
  server.send('Hello')

  return new Response(null, {
    status: 101,
    webSocket: client,
  })
})

export default app
wrangler dev websocket.ts --remote

@JustJoostNL
Copy link

Thanks!

@Kyiro
Copy link
Contributor

Kyiro commented Oct 26, 2023

It should be fairly trivial to make universal middleware for Workers, Deno and possibly Node. If I have the time, I could look into it as 3rd party middleware but no promise. Bun seems to be out of the question because of how their API works.
Useful links for reference:
Deno.upgradeWebSocket
Cloudflare
ws package for Node
Bun WebSockets

@yusukebe
Copy link
Member

Yeah, making a "universal" middleware is make sense.

@eriicafes
Copy link

Is there currently any restriction on manually implementing the connection upgrade on the route handler?
I’m wondering how I could implement it with the ws package for node, also wondering about Bun.

@Kyiro
Copy link
Contributor

Kyiro commented Oct 26, 2023

You have to use the node adapter with the handleUpgrade function, it'd definetly require some messing around with internals but it should be possible. As for Bun, you can put the upgrade function into Env from the hono server and in theory use WebSockets from the websocket handler in Bun.serve but the approach is completely different from Deno and Workers

@Kyiro
Copy link
Contributor

Kyiro commented Oct 30, 2023

WebSockets example for Node.js, it's way simpler than what I described

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { WebSocketServer } from 'ws';

const app = new Hono();

const server = serve(app, (info) => {
    console.log(`Listening on http://localhost:${info.port}`);
});

const wss = new WebSocketServer({ server });

wss.on("connection", function connection(ws) {
    ws.on("error", console.error);

    ws.on("message", function message(data) {
        console.log("received: %s", data);
    });

    ws.send("something");
});

@eriicafes
Copy link

Awesome this is exactly what I needed thank you 🎉

@m15r
Copy link

m15r commented Dec 12, 2023

I couldn't find any documentation on the WebSocket server included with Bun. This is how I got it working, but I'm sure there's a better way to handle upgrades.

import { Hono } from 'hono'

const app = new Hono()

Bun.serve({
    fetch: (req, server) => {
        if (server.upgrade(req)) {
            // handle authentication
        }
        return app.fetch(req, server)
    },
    websocket: {
        message(ws, message) {},
        open(ws) {
            ws.send('Hello!')
        },
        close(ws, code, message) {},
        drain(ws) {}
    },
    port: 8080
})

@patrickkabwe
Copy link

patrickkabwe commented Dec 21, 2023

For someone interested in using WebSockets via node server and socket.io check out this snippet

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import type { Server as HTTPSServer } from "node:http";
import { Server as SocketIOServer } from "socket.io";

const app = new Hono();

app.get("/", (c) => c.text("Hello Hono!"));

const server = serve(
  {
    fetch: app.fetch,
    port: 5000,
  },
  (add) => {
    console.log('Listening on http://localhost:'+${add.port});
  }
);

const io = new SocketIOServer(server as HTTPSServer);

io.on("connection", (socket) => {
  socket.emit("hello", "world");
  socket.on("hello", (data) => {
    socket.broadcast.emit("hello", data);
  });
});

@erf
Copy link

erf commented Jan 15, 2024

I would love to see a simpler way to handle WebSocket upgrade on Bun using a specific route (e.g. /ws) where you can handle events inside that route. For now as i described in this discussion, i was able to upgrade inside a route using the following:

app.get('/ws', (c) => {
  const success = c.env.upgrade(c.req.raw)
  return true
})

@erf
Copy link

erf commented Jan 15, 2024

Another example how to upgrade all WebSocket upgrade requests using Bun:

Bun.serve({
  port: process.env.PORT || 3000,
  fetch: (req, server) => {
    // check if request is websocket
    if (req.headers.get('upgrade') === 'websocket') {
      const success = server.upgrade(req);
      if (success) {
        // Bun automatically returns a 101 Switching Protocols
        // if the upgrade succeeds
        return undefined;
      }
    }
    // handle HTTP request normally
    return app.fetch(req, server)
  },
  websocket: webSocketHandler,
})

@danthegoodman1
Copy link

danthegoodman1 commented Jan 15, 2024

I would love to see a simpler way to handle WebSocket upgrade on Bun using a specific route (e.g. /ws) where you can handle events inside that route. For now as i described in this discussion, i was able to upgrade inside a route using the following:

app.get('/ws', (c) => {
  const success = c.env.upgrade(c.req.raw)
  return true
})

This worked in bun with typescript? That's undefined on c.env for me

@erf
Copy link

erf commented Jan 15, 2024

This worked in bun with typescript? That's undefined on c.env for me

I'm using JavaScript. You could try the other method i mentioned (inside the fetch call)

@danthegoodman1
Copy link

danthegoodman1 commented Jan 15, 2024

I'm using JavaScript. You could try the other method i mentioned (inside the fetch call)

Ah I understand why, for the typescript folks, you can do this:

const app = new Hono<{
  Bindings: {
    server: Server
  }
}>()

app.get("/ws", async (c) => {
  if (!c.env.server.upgrade(c.req.raw, {
    data: 'im some data for association'
  })) {
    logger.error("failed to upgrade!")
  }
  return new Response() // have to return empty response so hono doesn't get mad
})

Bun.serve({
  port: process.env.PORT || "8080",
  fetch: (req: Request, server: Server) => {
    return app.fetch(req, {
      server,
    })
  },
  websocket: {
    message(ws, msg) {
      console.log("got message", ws.data, msg)
    },
    open(ws) {
      console.log("websocket opened", ws.data)
    },
  },
)

was unaware of req.raw before, now I know!

@erf
Copy link

erf commented Jan 16, 2024

Nice you found out! I did a console.log(c) to learn about the internals .. however not sure env is supposed to be used for this purpose??

@danthegoodman1
Copy link

It's because when you do the shortcut of fetch: app.fetch you are passing in (req, server), with server being the object that is assigned to the .env of hono. Because .upgrade is a method on server, you are effectively making c.env equivalent to the server argument.

This is the type for Hono.fetch

fetch: (request: Request, Env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => Response | Promise<Response>;

@Kyiro
Copy link
Contributor

Kyiro commented Jan 16, 2024

I don't think it's possible to handle upgrades inside the route handler itself in Bun, but it's very doable in Node. I've made a PR to address this a while ago but I didn't have much time to update it so it's stale for now.

@erf
Copy link

erf commented Jan 16, 2024

I really like how this is done in Elysia

@Kyiro
Copy link
Contributor

Kyiro commented Jan 17, 2024

Elysia has the advantage of supporting only Bun in this case because implementing a ws handler function would require some different code for every runtime. My plan was to make a helper like this without Bun support but I've yet to PR anything.

import { upgradeWebSocket } from "hono/ws";
import { Hono } from "hono";

const app = new Hono();

app.get("/ws", (c) => upgradeWebSocket(c, (ws) => {}));

@erf
Copy link

erf commented Jan 17, 2024

Elysia has the advantage of supporting only Bun in this case ..

I see. Hono has to do more work to support multiple runtimes. Maybe Elysia is a better if only building for Bun.

@tobowers
Copy link

If you want to have "normal" websockets (with listeners) in Bun/hono I made this little bit of code: https://gist.github.com/tobowers/2117365c8210d1758a4c6e2f859619c0

@Kyiro
Copy link
Contributor

Kyiro commented Jan 18, 2024

If you want to have "normal" websockets (with listeners) in Bun/hono I made this little bit of code: https://gist.github.com/tobowers/2117365c8210d1758a4c6e2f859619c0

That's pretty nice!

@erf
Copy link

erf commented Jan 18, 2024

If you want to have "normal" websockets (with listeners) in Bun/hono

I'd rather have the 7x performance by Bun :)

@tobowers
Copy link

If you want to have "normal" websockets (with listeners) in Bun/hono

I'd rather have the 7x performance by Bun :)

Sure, but most libraries can't handle it.

@erf
Copy link

erf commented Jan 18, 2024

Sure, but most libraries can't handle it.

What libraries are you referring to? (I only need a simple web framework / library which can handle routes and WebSocket but is preferably "cross-runtime" and works with Bun)

@tobowers
Copy link

I mean libraries consuming websockets

@erf
Copy link

erf commented Jan 18, 2024

I mean libraries consuming websockets

Not aware of any such libraries. Just need a standard WebSocket API

@yusukebe
Copy link
Member

Now, we have WebSocket Helper / Adapter! #2265

@oyal
Copy link

oyal commented Sep 1, 2024

For someone interested in using WebSockets via node server and socket.io check out this snippet对于有兴趣通过节点服务器和 socket.io 使用 WebSockets 的人,请查看此代码片段

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import type { Server as HTTPSServer } from "node:http";
import { Server as SocketIOServer } from "socket.io";

const app = new Hono();

app.get("/", (c) => c.text("Hello Hono!"));

const server = serve(
  {
    fetch: app.fetch,
    port: 5000,
  },
  (add) => {
    console.log('Listening on http://localhost:'+${add.port});
  }
);

const io = new SocketIOServer(server as HTTPSServer);

io.on("connection", (socket) => {
  socket.emit("hello", "world");
  socket.on("hello", (data) => {
    socket.broadcast.emit("hello", data);
  });
});

May I ask how to use io in a separate routing file?

I don't know how to implement similar functions in hono.

I implemented it in Express like this:

// server.ts
import express from 'express'
import http from 'http'

const app = express()
const server = http.createServer(app)

const io = new Server(server, {
  cors: {
    origin: '*',
  },
})


io.on('connection', (socket) => {
  console.log('a user connected')
})

app.set('io', io)

app.use('/messages', authenticate, messageRouter)
// messages.ts
import express from 'express'

const router = express.Router()

router.get('/', (
  req: express.Request,
  res: express.Response,
) => {
  const io = req.app.get('io')
  io.emit('message', 'hello1')
 // ...
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests