Deno: How to use WebSocket with oak?

Viewed 2577

As Deno was released last Wednesday, I tried to play with it and redo the little example Chat App, I tried this:

import { Application, Router, send } from 'https://deno.land/x/oak/mod.ts';
import { listenAndServe } from 'https://deno.land/std/http/server.ts'

const app = new Application();
const router = new Router();

router
  .get('/ws', handleSocket);


app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: HTTP_PORT });

app.ts

import { WebSocket, acceptWebSocket, isWebSocketCloseEvent, acceptable } from 'https://deno.land/std/ws/mod.ts'
import { v4 } from 'https://deno.land/std/uuid/mod.ts'

const users = new Map<string, WebSocket>()

export const handleSocket = async (ctx: any) => {
  if (acceptable(ctx.request.serverRequest)) {
    const { conn, r: bufReader, w: bufWriter, headers } = ctx.request.serverRequest;
    const socket = await acceptWebSocket({
      conn,
      bufReader,
      bufWriter,
      headers,
    });

    await socketEventHandlers(socket);
  } else {
    throw new Error('Error when connecting websocket');
  }
}
...

export const socketEventHandlers = async (ws: WebSocket): Promise<void> => {
  // Register user connection
  const userId = v4.generate()

  users.set(userId, ws)
  await broadcast(`> User with the id ${userId} is connected`)

  // Wait for new messages
  for await (const event of ws) {
    const message = typeof event === 'string' ? event : ''

    await broadcast(message, userId)

    // Unregister user conection
    if (!message && isWebSocketCloseEvent(event)) {
      users.delete(userId)
      await broadcast(`> User with the id ${userId} is disconnected`)
    }
  }
}

socket.ts

The websocket connection works perfectly with the import { listenAndServe } from 'https://deno.land/std/http/server.ts' , but with the code above I got errors like WebSocket connection to 'ws://localhost:3000/ws' failed: Invalid frame header.

Does anybody have any tips to solve it? Thx ;)

3 Answers

TL;DR - This has been updated since answer was accepted and is much simpler now.

router.get('/ws', async ctx => {
    const sock = await ctx.upgrade();
    handleSocket(sock);
});

Credit https://github.com/oakserver/oak/pull/137

The issue happens because you're using the wrong version of the libraries. Always use versioned URLs in Deno.

For Deno 1.0.0, you'll need to use oak v4.0.0 & std v0.51.0

app.ts

import { Application, Router, send } from 'https://deno.land/x/oak@v4.0.0/mod.ts';

socket.ts

import { WebSocket, acceptWebSocket, isWebSocketCloseEvent, acceptable } from 'https://deno.land/std@0.51.0/ws/mod.ts'
import { v4 } from 'https://deno.land/std@0.51.0/uuid/mod.ts'

Once you make those changes, you'll be able to connect correctly to the WebSocket Server.

const ws = new WebSocket("ws://127.0.0.1:8080/ws")
ws.onopen = function () {
  ws.send('OAK is working!')
}

In solving this same problem of upgrading http connections to a websocket on the same port the following overly simplified solution works with Deno 1.2 and Oak.

/*
running with Deno 1.2
deno run --inspect --allow-net ./beautiful-socket.js
*/
import { Application, Router, HttpError, send, Status } from "https://deno.land/x/oak@v6.0.1/mod.ts";
import { createWebSocket, isWebSocketCloseEvent, acceptWebSocket, acceptable } from "https://deno.land/std@0.61.0/ws/mod.ts";

const port = 8123;
const users = new Set();
const app = new Application({state:{users}});
const router = new Router();

function broadcastEach(user){
    user.send(this);
}
function broadcast(msg){
    console.log('---broadcasting--->', typeof msg, msg);
    users.forEach(broadcastEach, msg);
}

router.get('/socket', async (context, next) => {
    if( !context.isUpgradable ){
        throw new Error('bummers opening socket :(');
    }
    const socket = await context.upgrade();
    users.add(socket);
    broadcast(`hello! ${ socket.conn.rid }`);
    for await (const ev of socket) {
        if(isWebSocketCloseEvent(ev)){
            users.delete(socket);
            broadcast(`bye! ${ socket.conn.rid }`);
        }else{
            broadcast(ev);
        };
    }
});

router.get('/', async (context) => {
    context.response.body = `<!doctype html>
<html><body>
<p>let's chat...open the console to chat it up</p>
<script>
console.log(123);
const pipe = new WebSocket("ws://${context.request.url.host}/socket");
function fire(ev){
    switch(ev.type){
    case 'message':
        switch(typeof ev.data){
        case 'string':
            console.log('msg text', ev.data);
        break;
        case 'object':
            ev.data.arrayBuffer().then(ab=>{ console.log(new Uint8Array(ab)); });
        break;
        }
    break;
    default:
        console.log(ev.type ,ev);
    }
}
function hello(msg){
    if(msg === undefined){
        msg = new ArrayBuffer(4);
        const uint = new Uint8Array(msg);
        uint[0] = 4;
        uint[1] = 3;
        uint[2] = 2;
        uint[3] = 1;
    }
    pipe.send(msg);
}

pipe.addEventListener('open', fire);
pipe.addEventListener('close', fire);
pipe.addEventListener('message', fire);
pipe.addEventListener('error', fire);


</script>
</body></html>
`;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.addEventListener('error', (ev)=>{
    console.error(ev);
    debugger;
});

app.addEventListener('listen', (server)=>{
    console.log(`open ${ server.secure ? 'https':'http' }://${ server.hostname }:${ server.port }`);
});
const whenClosed = app.listen(`:${port}`);
await whenClosed;
console.log(`closed http :${port}, bye`);
Related