From 45d6f6ff83a3088e7d3e68514cf0f486c3661865 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:11:54 +0100 Subject: [PATCH] fix: include default headers in realtime websocket handshakes --- .../resources/beta/realtime/realtime.py | 2 + src/openai/resources/realtime/realtime.py | 2 + tests/lib/test_websocket_redirects.py | 45 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/openai/resources/beta/realtime/realtime.py b/src/openai/resources/beta/realtime/realtime.py index 7124287c0e..cd850de319 100644 --- a/src/openai/resources/beta/realtime/realtime.py +++ b/src/openai/resources/beta/realtime/realtime.py @@ -381,6 +381,7 @@ async def __aenter__(self) -> AsyncRealtimeConnection: user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { + **self.__client._custom_headers, **auth_headers, "OpenAI-Beta": "realtime=v1", }, @@ -564,6 +565,7 @@ def __enter__(self) -> RealtimeConnection: user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { + **self.__client._custom_headers, **auth_headers, "OpenAI-Beta": "realtime=v1", }, diff --git a/src/openai/resources/realtime/realtime.py b/src/openai/resources/realtime/realtime.py index 3cb815bddf..e6bd480b25 100644 --- a/src/openai/resources/realtime/realtime.py +++ b/src/openai/resources/realtime/realtime.py @@ -715,6 +715,7 @@ async def _connect_ws(self, extra_query: Query, extra_headers: Headers) -> Async user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { + **self.__client._custom_headers, **auth_headers, }, extra_headers, @@ -1183,6 +1184,7 @@ def _connect_ws(self, extra_query: Query, extra_headers: Headers) -> WebSocketCo user_agent_header=self.__client.user_agent, additional_headers=_merge_mappings( { + **self.__client._custom_headers, **auth_headers, }, extra_headers, diff --git a/tests/lib/test_websocket_redirects.py b/tests/lib/test_websocket_redirects.py index 2ae84e6bfb..c605f423a5 100644 --- a/tests/lib/test_websocket_redirects.py +++ b/tests/lib/test_websocket_redirects.py @@ -134,6 +134,28 @@ async def test_async_websocket_redirects( assert all(uri.host == "origin.test" for uri, _ in handshakes.sent) +@pytest.mark.parametrize("name", ["realtime", "beta.realtime"]) +async def test_async_realtime_websocket_includes_default_headers(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + handshakes = Handshakes(monkeypatch, [None]) + async with AsyncOpenAI( + api_key="fake-key", + websocket_base_url="wss://origin.test", + default_headers={ + "X-Proxy-Auth": "proxy-token", + "X-Custom": "default-value", + }, + http_client=async_http_client(), + ) as client: + async with resource(client, name).connect(**options(name)): + pass + + assert len(handshakes.attempts) == 1 + headers = handshakes.attempts[0][1] + assert headers["X-Proxy-Auth"] == "proxy-token" + assert headers["X-Custom"] == EXTRA_HEADERS["X-Custom"] + assert headers["Authorization"] == "Bearer fake-key" + + @pytest.mark.skipif(not FOLLOWS_REDIRECTS, reason="No automatic handshake redirects") @pytest.mark.parametrize("name", RESOURCES) async def test_later_cross_origin_redirect_is_rejected(monkeypatch: pytest.MonkeyPatch, name: str) -> None: @@ -220,6 +242,29 @@ def test_sync_websocket_connector_is_unchanged(monkeypatch: pytest.MonkeyPatch, assert connect_mock.call_args.kwargs["additional_headers"]["Authorization"] == "Bearer fake-key" +@pytest.mark.parametrize("name", ["realtime", "beta.realtime"]) +def test_sync_realtime_websocket_includes_default_headers(monkeypatch: pytest.MonkeyPatch, name: str) -> None: + websocket = Mock() + connect_mock = Mock(return_value=websocket) + monkeypatch.setattr("websockets.sync.client.connect", connect_mock) + with OpenAI( + api_key="fake-key", + websocket_base_url="wss://origin.test", + default_headers={ + "X-Proxy-Auth": "proxy-token", + "X-Custom": "default-value", + }, + http_client=httpx2.Client(transport=httpx2.MockTransport(unexpected_http)), + ) as client: + with resource(client, name).connect(**options(name)): + pass + + headers = connect_mock.call_args.kwargs["additional_headers"] + assert headers["X-Proxy-Auth"] == "proxy-token" + assert headers["X-Custom"] == EXTRA_HEADERS["X-Custom"] + assert headers["Authorization"] == "Bearer fake-key" + + @pytest.mark.skipif(not FOLLOWS_REDIRECTS, reason="No automatic handshake redirects") def test_non_redirect_error_is_preserved() -> None: error = InvalidStatus(Response(401, "Unauthorized", Headers()))