Skip to content

πŸ› Bug Report β€” closing Durable Object websocket doesn't trigger close eventΒ #4327

Open
@ruifigueira

Description

@ruifigueira

I'm establishing a websocket connection with a durable object. In the client side (worker), if I explicitly close the websocket, no close event is triggered.

If I obtain a websocket with new WebSocket(...), for instance to connect with an external websocket, it works as expected: explicitly closing the websocket triggers the close event.

To reproduce the issue:

  • create a worker project
  • edit src/index.ts:
import { DurableObject } from "cloudflare:workers";

export class TestDurableObject extends DurableObject<Env> {
  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
  }

  async fetch(request: Request) {
    if (request.headers.get("Upgrade") !== "websocket")
      return new Response("Expected WebSocket request", { status: 400 });

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

    server.accept();

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

export default {
  async fetch(request, env): Promise<Response> {
    const url = new URL(request.url);

    let ws: WebSocket;
    if (url.searchParams.has('ws')) {
      // example: http://127.0.0.1:8787/?ws=wss://echo.websocket.org/
      ws = new WebSocket(url.searchParams.get('ws')!);
    } else {
      const id = env.TEST_DO.idFromName('test-instance');
      const obj = env.TEST_DO.get(id);
      const response = await obj.fetch(request, {
        headers: {
          "Upgrade": "websocket"
        }
      });
      ws = response.webSocket!;
      ws.accept();
    }

    const logs: string[] = [];

    const { promise, resolve } = Promise.withResolvers<void>();

    ws.addEventListener('open', () => {
      logs.push('Connection opened');
      ws.close(1000);
    });
    ws.addEventListener('close', ({ code, reason }) => {
      logs.push(`Connection closed: ${JSON.stringify({ code, reason })}`);
      resolve();
    });

    await promise;

    return new Response(logs.join('\n'), { status: 200 });
  },
} satisfies ExportedHandler<Env>;
  • edit wrangler.jsonc:
{
  "name": "websocket-close-test",
  "main": "src/index.ts",
  "compatibility_date": "2025-05-25",
  "compatibility_flags": [
    "nodejs_compat",
  ],
  "observability": {
    "enabled": true
  },
  "durable_objects": {
    "bindings": [
      {
        "name": "TEST_DO",
        "class_name": "TestDurableObject"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["TestDurableObject"]
    }
  ],
}
Connection opened
Connection closed: {"code":1000,"reason":""}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions