From 8b67ab52c52582521647eff6bfde975541d29c63 Mon Sep 17 00:00:00 2001 From: Paul Tancre Date: Wed, 1 Jul 2026 15:39:17 +0100 Subject: [PATCH] fix: strip /v1/proxy suffix when invoking functions via proxy_conn Function invocation, sandbox listing, and code push all POST through proxy_conn. For EU/self-hosted orgs the login response sets proxy_url to `{api_url}/v1/proxy`, but these `function/*` endpoints are served at the API host root, so requests hit `.../v1/proxy/function/invoke` and return 405 "Method POST not supported". US multi-tenant orgs avoid this only because their proxy_url falls back to the bare api_url. Normalize the proxy_conn base URL by stripping a trailing /v1/proxy. state.proxy_url is left untouched so nothing else that reads it changes. Co-Authored-By: Claude Opus 4.8 --- py/src/braintrust/logger.py | 14 +++++++++++++- py/src/braintrust/test_logger.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/py/src/braintrust/logger.py b/py/src/braintrust/logger.py index 9a65dee3..3b2532d5 100644 --- a/py/src/braintrust/logger.py +++ b/py/src/braintrust/logger.py @@ -382,6 +382,18 @@ def __exit__( NOOP_SPAN: Span = _NoopSpan() NOOP_SPAN_PERMALINK = "https://www.braintrust.dev/noop-span" +_V1_PROXY_SUFFIX = "/v1/proxy" + + +def _normalize_proxy_conn_url(proxy_url: str) -> str: + # proxy_url may point at the universal proxy (`{api_url}/v1/proxy`) for + # EU/self-hosted orgs, but proxy_conn only targets Braintrust API endpoints + # (e.g. function/invoke, function/sandbox-list) served at the API host root. + # Drop the suffix so these requests resolve on all data planes. + if proxy_url.endswith(_V1_PROXY_SUFFIX): + return proxy_url[: -len(_V1_PROXY_SUFFIX)] + return proxy_url + class BraintrustState: def __init__(self): @@ -609,7 +621,7 @@ def proxy_conn(self): if not self._proxy_conn: if not self.proxy_url: raise RuntimeError("Must initialize proxy_url before requesting proxy_conn") - self._proxy_conn = HTTPConnection(self.proxy_url, adapter=_http_adapter) + self._proxy_conn = HTTPConnection(_normalize_proxy_conn_url(self.proxy_url), adapter=_http_adapter) return self._proxy_conn def user_info(self) -> Mapping[str, Any]: diff --git a/py/src/braintrust/test_logger.py b/py/src/braintrust/test_logger.py index 3fc5c833..459f4b06 100644 --- a/py/src/braintrust/test_logger.py +++ b/py/src/braintrust/test_logger.py @@ -3868,6 +3868,20 @@ def test_check_org_info_with_git_metadata_uses_server_settings(): assert set(state.git_metadata_settings.fields) == {"commit", "branch"} +def test_proxy_conn_strips_v1_proxy_suffix(): + """EU/self-hosted proxy_url ends in /v1/proxy; proxy_conn must target the API host root.""" + state = BraintrustState() + state.proxy_url = "https://api-eu.braintrust.dev/v1/proxy" + assert state.proxy_conn().base_url == "https://api-eu.braintrust.dev" + + +def test_proxy_conn_leaves_bare_host_unchanged(): + """A bare proxy host (US default) is used as-is.""" + state = BraintrustState() + state.proxy_url = "https://api.braintrust.dev" + assert state.proxy_conn().base_url == "https://api.braintrust.dev" + + def test_get_repo_info_without_settings_returns_none(): """Direct call to get_repo_info with settings=None should return None.""" assert get_repo_info(None) is None