oak icon indicating copy to clipboard operation
oak copied to clipboard

Bug: `deno serve` causes websocket upgrade to fail

Open jkonowitch opened this issue 1 year ago • 3 comments

Code:

// main.ts
import { Application, Router } from '@oak/oak';

const router = new Router();

router.get('/wss', (context) => {

  const socket = context.upgrade();
  socket.onmessage = (event) => {
    console.log(event.data);
    socket.send('Hello, WebSocket! [Server]');
  };
});

router.get('/', (context) => {
  context.response.body = `
    <h1>Hello, Oak!</h1>
    <script>
      const ws = new WebSocket('ws://localhost:8000/wss');
      ws.onopen = () => {
        console.log('WebSocket connection established!');
        ws.send('Hello, WebSocket!');
      };
      ws.onmessage = (event) => {
        console.log(event.data);
      };
    </script>
  `;
  context.response.type = 'text/html';
});

const app = new Application();

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

export default { fetch: app.fetch };

deno serve main.ts

Expected Result:

Success

Actual Result:

deno serve: Listening on http://0.0.0.0:8000/
Upgrade response was not returned from callback

Note:

This works correctly when using Hono and following their Deno instructions:

image

jkonowitch avatar Jan 09 '25 16:01 jkonowitch

As a follow up, by using the underlying Deno APIs, this problem can be worked around:

// this works
import { Application, Router } from "@oak/oak";

const router = new Router();

router.get("/wss", (context) => {
  const { socket, response } = Deno.upgradeWebSocket(context.request.source!);
  socket.onmessage = (event) => {
    console.log(event.data);
    socket.send("Hello, WebSocket! [Server]");
  };
  context.response.with(response);
});

router.get("/", (context) => {
  context.response.body = `
    <h1>Hello, Oak!</h1>
    <script>
      const ws = new WebSocket('ws://localhost:8000/wss');
      ws.onopen = () => {
        console.log('WebSocket connection established!');
        ws.send('Hello, WebSocket!');
      };
      ws.onmessage = (event) => {
        console.log(event.data);
      };
    </script>
  `;
  context.response.type = "text/html";
});

const app = new Application();

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

export default {
  fetch: app.fetch,
};

jkonowitch avatar Jan 09 '25 17:01 jkonowitch

Thanks for the reproduction (and work around). I have never validated oak against a deno serve use case actually but need to do that.

kitsonk avatar Jan 09 '25 23:01 kitsonk

FYI, I also tested SSE, and there is a similar issue with deno serve

import { Application, Context, Router, Status } from "@oak/oak";

const router = new Router();

const html = `
<!DOCTYPE html>
<html>
  <head>
    <title>Server-Sent Events</title>
    <script src="https://unpkg.com/[email protected]"></script>
    <script src="https://unpkg.com/[email protected]/sse.js"></script>
  </head>
  <body>
    <div hx-ext="sse" sse-connect="/sse" sse-swap="message"></div>
  </body>
</html>
`;

router.get(
  "/",
  (ctx) => {
    ctx.response.body = html;
  },
).get(
  "/sse",
  async (ctx: Context) => {
    ctx.assert(
      ctx.request.accepts("text/event-stream"),
      Status.UnsupportedMediaType,
    );

    const target = await ctx.sendEvents();
    target.dispatchMessage("<h2>hello</h2>");
  },
);

const app = new Application();

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

export default { fetch: app.fetch };

Expected Result

<h2> shows up in the DOM

Actual Result

404 Error

Notes

This example works fine when using app.listen

jkonowitch avatar Jan 19 '25 18:01 jkonowitch