Skip to content

Python: [Bug]: TypeSafe connector ignores HTTPS_PROXY / HTTP_PROXY / NO_PROXY when it creates its own SDK client #8983

Closed

Description

Description

When TypeSafeChatClient (or RawTypeSafeChatClient) creates its own AsyncTypeSafeClient, it passes transport=_ApiKeyTransport(api_key_value) (python/packages/typesafe/agent_framework_typesafe/_chat_client.py, L189). The SDK hands that transport to httpx2.AsyncClient, and httpx2 reads proxy environment variables only when no custom transport is given (allow_env_proxies = trust_env and transport is None in httpx2/_client.py). _ApiKeyTransport wraps a plain httpx2.AsyncHTTPTransport(), so HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY are ignored and every request is sent directly.

Behind a corporate proxy, the connector cannot reach TypeSafe, while the TypeSafe SDK on its own works with the same environment. The only workaround today is to build an AsyncTypeSafeClient yourself and inject it.

The custom transport only sets Authorization: Bearer <api_key>. The SDK already sends exactly this header. With typesafe-sdk 0.7.1 and 0.7.2 (the range allowed by pyproject.toml; their source is identical), a wire capture without _ApiKeyTransport shows Authorization: Bearer <api_key>. The redaction mentioned in the _ApiKeyTransport docstring and in AGENTS.md only applies to log records and exception messages (typesafe_sdk/_core/logging.py), not to the request on the wire. The SDK already sending the bearer header was also noted during the #8592 review.

Steps to reproduce: run the code sample below. It starts a fake proxy on localhost that records each request line and refuses it, sets HTTPS_PROXY, then calls the same unresolvable endpoint (https://typesafe.invalid, so nothing leaves the machine) once with AsyncTypeSafeClient and once with TypeSafeChatClient.

Actual: the SDK client sends its request and its retries through the proxy. The connector never contacts the proxy and fails on a direct DNS lookup (see the output below).

Expected: a connector-owned client honors the standard proxy environment variables, like AsyncTypeSafeClient does. With _ApiKeyTransport removed, the connector behaves like the SDK: the proxy sees the three CONNECT lines and the error becomes Connection error: 403 Forbidden.

Proposed fix: remove _ApiKeyTransport and let the SDK build its default transport, update the AGENTS.md note, and keep a wire-level test for the outgoing Authorization header. A PR with the fix and regression tests is ready.

Code Sample

import asyncio
import os

from agent_framework import Message
from agent_framework_typesafe import TypeSafeChatClient
from typesafe_sdk import AsyncTypeSafeClient, Noul

proxied: list[str] = []


async def fake_proxy(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
    # Record the request line, then refuse the request.
    proxied.append((await reader.readline()).decode().strip())
    while (await reader.readline()) not in (b"\r\n", b""):
        pass
    writer.write(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
    await writer.drain()
    writer.close()


async def main() -> None:
    proxy = await asyncio.start_server(fake_proxy, "127.0.0.1", 0)
    for name in ("HTTP_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"):
        os.environ.pop(name, None)
    os.environ["HTTPS_PROXY"] = f"http://127.0.0.1:{proxy.sockets[0].getsockname()[1]}"
    base_url = "https://typesafe.invalid"  # never resolves, so nothing leaves the machine
    questions = {"urgent": Noul(instructions="Is this urgent?")}

    async with AsyncTypeSafeClient(api_key="test-key", base_url=base_url) as sdk_client:
        try:
            await sdk_client.system_one(state="hello", questions=questions)
        except Exception as exc:
            print(f"SDK client:       {type(exc).__name__}, proxy saw {proxied}")
    proxied.clear()

    async with TypeSafeChatClient(api_key="test-key", base_url=base_url) as client:
        try:
            await client.get_response([Message("user", ["hello"])], options={"response_format": questions})
        except Exception as exc:
            print(f"Connector client: {type(exc).__name__}, proxy saw {proxied}")
            print(f"  {exc.args[0]}")
    proxy.close()


asyncio.run(main())

Error Messages / Stack Traces

SDK client:       TypeSafeAPIConnectionError, proxy saw ['CONNECT typesafe.invalid:443 HTTP/1.1', 'CONNECT typesafe.invalid:443 HTTP/1.1', 'CONNECT typesafe.invalid:443 HTTP/1.1']
Connector client: ChatClientException, proxy saw []
  TypeSafe request failed: Connection error: [Errno -2] Name or service not known

Package Versions

agent-framework-typesafe: 1.0.0a261002, agent-framework-core: 1.20.0 (main @ e9857bd), typesafe-sdk: 0.7.2 (0.7.1 checked too), httpx2: 2.13.1

Python Version

Python 3.12

Additional Context

The SDK also overwrites the User-Agent header that the connector passes (headers={"User-Agent": get_user_agent()}), so the value on the wire is typesafe-sdk/0.7.2. That is a separate topic and is not changed by the proposed fix.

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: Python

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions