Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/core/websocket/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import { RUNTIME } from "../runtime/index.js";
import { toQueryString } from "../url/qs.js";
import * as Events from "./events.js";

const getGlobalWebSocket = (): WebSocket | undefined => {
export const getGlobalWebSocket = (): WebSocket | undefined => {
// Server runtimes must use the `ws` package (NodeWebSocket) because their
// native WebSockets (Bun, Node 21+) do not support the 3rd `options` argument for headers.
if (RUNTIME.type === "node" || RUNTIME.type === "bun") {
return NodeWebSocket as unknown as WebSocket;
}

// Fallback to the environment's native WebSocket (Browser, Edge, Deno, etc.)
if (typeof WebSocket !== "undefined") {
// @ts-ignore
return WebSocket;
} else if (RUNTIME.type === "node") {
return NodeWebSocket as unknown as WebSocket;
}

return undefined;
};

Expand Down
84 changes: 84 additions & 0 deletions tests/unit/websocket-bun-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { getGlobalWebSocket } from "../../src/core/websocket/ws.js";
import { WebSocket as NodeWebSocket } from "ws";

/**
* Tests for WebSocket runtime selection in Bun environments.
*
* Bun's native WebSocket constructor ignores the 3rd `options` argument,
* which means custom headers (including `Authorization`) are silently
* dropped. This causes authentication failures when connecting to
* Deepgram's WebSocket API.
*
* The fix ensures that `getGlobalWebSocket()` returns `NodeWebSocket`
* (from the `ws` library) instead of Bun's native `WebSocket` when
* running in a server runtime.
*
* Related issue: #466
*/

// Mock the RUNTIME object
vi.mock("../../src/core/runtime/index.js", () => {
return {
RUNTIME: {
type: "browser"
}
};
});

import { RUNTIME } from "../../src/core/runtime/index.js";

describe("WebSocket runtime selection logic", () => {
let originalWebSocket: any;

beforeEach(() => {
originalWebSocket = (globalThis as any).WebSocket;
});

afterEach(() => {
if (originalWebSocket !== undefined) {
(globalThis as any).WebSocket = originalWebSocket;
} else {
delete (globalThis as any).WebSocket;
}
});

it("should return NodeWebSocket when runtime is bun", () => {
RUNTIME.type = "bun";
(globalThis as any).WebSocket = class NativeBunWebSocket {};

const wsClass = getGlobalWebSocket();

expect(wsClass).toBe(NodeWebSocket);
expect(wsClass).not.toBe((globalThis as any).WebSocket);
});

it("should return NodeWebSocket when runtime is node", () => {
RUNTIME.type = "node";
(globalThis as any).WebSocket = class NativeNodeWebSocket {}; // Simulating Node 21+

const wsClass = getGlobalWebSocket();

expect(wsClass).toBe(NodeWebSocket);
});

it("should return native WebSocket when runtime is browser", () => {
RUNTIME.type = "browser";
const MockNativeWebSocket = class {};
(globalThis as any).WebSocket = MockNativeWebSocket;

const wsClass = getGlobalWebSocket();

expect(wsClass).toBe(MockNativeWebSocket);
expect(wsClass).not.toBe(NodeWebSocket);
});

it("should return undefined when no WebSocket is available and runtime is unknown", () => {
RUNTIME.type = "unknown";
delete (globalThis as any).WebSocket;

const wsClass = getGlobalWebSocket();

expect(wsClass).toBeUndefined();
});
});