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
2 changes: 2 additions & 0 deletions src/openai/resources/beta/realtime/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down
2 changes: 2 additions & 0 deletions src/openai/resources/realtime/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +718 to 719

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Merge authentication headers case-insensitively

When a caller supplies a differently cased authentication key such as default_headers={"authorization": "Bearer stale"}, this case-sensitive dictionary expansion retains both that value and the generated Authorization header. WebSocket header names are case-insensitive, so the handshake receives duplicate credentials rather than the intended generated-auth precedence and may be rejected; the same pattern occurs in the sync and beta paths. Normalize header names or remove case-insensitive collisions before connecting, and cover this case in both sync and async authentication tests.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

},
extra_headers,
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions tests/lib/test_websocket_redirects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()))
Expand Down