From 49024462f39e79a3b477306f46ea2f9f1dd18e73 Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:13:27 +0100 Subject: [PATCH] Studio: ignore unsupported env proxy during Studio startup (#6102) * fix: ignore unsupported env proxy during Studio startup * fix: handle missing socksio env proxy at startup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Match printf logging style and inline the proxy predicate for PR #6102 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- .../core/inference/external_provider.py | 21 ++++- .../tests/test_external_provider_proxy_env.py | 80 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 studio/backend/tests/test_external_provider_proxy_env.py diff --git a/studio/backend/core/inference/external_provider.py b/studio/backend/core/inference/external_provider.py index a7d44b992d..cae001c34d 100644 --- a/studio/backend/core/inference/external_provider.py +++ b/studio/backend/core/inference/external_provider.py @@ -469,9 +469,24 @@ def _apply_mistral_reasoning_controls( # Shared client reused across all requests for HTTP connection pooling. -# Auth headers and timeouts are per-request, so one client handles every -# provider without storing credentials. -_http_client = httpx.AsyncClient() +# Auth headers and timeouts are passed per-request, so a single client +# handles every provider without storing credentials. +def _create_shared_http_client() -> httpx.AsyncClient: + # Unsupported env proxy schemes (socks:// etc) raise at construction and + # would crash Studio startup (#6090); retry ignoring env proxies instead. + try: + return httpx.AsyncClient() + except (ImportError, ValueError) as exc: + exc_str = str(exc) + if "Unknown scheme for proxy URL" not in exc_str and "socksio" not in exc_str: + raise + logger.warning( + "Ignoring unsupported environment proxy for the shared HTTP client: %s", exc_str + ) + return httpx.AsyncClient(trust_env = False) + + +_http_client = _create_shared_http_client() # Cap per-image fetch well below Gemini's ~20 MB total request budget. diff --git a/studio/backend/tests/test_external_provider_proxy_env.py b/studio/backend/tests/test_external_provider_proxy_env.py new file mode 100644 index 0000000000..f17b655908 --- /dev/null +++ b/studio/backend/tests/test_external_provider_proxy_env.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +from pathlib import Path +import importlib.util +import sys + +_BACKEND_DIR = str(Path(__file__).resolve().parent.parent) +if _BACKEND_DIR not in sys.path: + sys.path.insert(0, _BACKEND_DIR) + +_EXTERNAL_PROVIDER_PATH = ( + Path(__file__).resolve().parent.parent / "core/inference/external_provider.py" +) + + +def _load_external_provider_module(): + spec = importlib.util.spec_from_file_location( + "external_provider_under_test", + _EXTERNAL_PROVIDER_PATH, + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_shared_http_client_ignores_unsupported_proxy_scheme(monkeypatch): + ep_mod = _load_external_provider_module() + calls = [] + + class FakeAsyncClient: + def __init__(self, **kwargs): + calls.append(kwargs) + if kwargs.get("trust_env") is not False: + raise ValueError("Unknown scheme for proxy URL URL('socks4://127.0.0.1:12345')") + + monkeypatch.setattr(ep_mod.httpx, "AsyncClient", FakeAsyncClient) + + client = ep_mod._create_shared_http_client() + + assert isinstance(client, FakeAsyncClient) + assert calls == [{}, {"trust_env": False}] + + +def test_shared_http_client_ignores_missing_socksio(monkeypatch): + ep_mod = _load_external_provider_module() + calls = [] + + class FakeAsyncClient: + def __init__(self, **kwargs): + calls.append(kwargs) + if kwargs.get("trust_env") is not False: + raise ImportError( + "Using SOCKS proxy, but the 'socksio' package is not installed. " + "Make sure to install httpx using `pip install httpx[socks]`." + ) + + monkeypatch.setattr(ep_mod.httpx, "AsyncClient", FakeAsyncClient) + + client = ep_mod._create_shared_http_client() + + assert isinstance(client, FakeAsyncClient) + assert calls == [{}, {"trust_env": False}] + + +def test_shared_http_client_reraises_other_value_errors(monkeypatch): + ep_mod = _load_external_provider_module() + + class FakeAsyncClient: + def __init__(self, **kwargs): + raise ValueError("different httpx setup error") + + monkeypatch.setattr(ep_mod.httpx, "AsyncClient", FakeAsyncClient) + + try: + ep_mod._create_shared_http_client() + except ValueError as exc: + assert str(exc) == "different httpx setup error" + else: + raise AssertionError("expected ValueError")