fix: 修复前端代理服务器设置清空保存后,httpx 持续报 `Unknown scheme for proxy URL (#5899)

This commit is contained in:
ui_beam
2026-06-05 15:20:31 +08:00
committed by GitHub
parent d0b62523a0
commit f4011d3ac2
2 changed files with 35 additions and 9 deletions

View File

@@ -9,10 +9,10 @@ import sys
import threading
from asyncio import AbstractEventLoop
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_origin, get_args
from urllib.parse import quote, urlencode, urlparse
from dotenv import set_key
from dotenv import set_key, unset_key
from pydantic import BaseModel, Field, ConfigDict, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -690,6 +690,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
if isinstance(value, str):
value = value.strip()
# 处理 Optional 类型:当值为空字符串且类型允许 None 时,转为 None
# 兼容 typing.Union (Python 3.9) 与 types.UnionType (Python 3.10+ PEP 604)
origin = get_origin(expected_type)
is_union = origin is Union or getattr(origin, "__name__", None) == "UnionType"
if (
is_union
and type(None) in get_args(expected_type)
and isinstance(value, str)
and not value
):
return default, str(default) != str(original_value)
try:
if expected_type is bool:
if isinstance(value, bool):
@@ -812,13 +824,19 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
logger.warning(message)
return False, message
else:
# 当值为 None 时,从 env 文件中删除该键,恢复为默认值
if converted_value is None:
unset_key(
dotenv_path=SystemUtils.get_env_path(),
key_to_unset=field_name,
)
logger.info(f"配置项 '{field_name}' 已清空,从 'app.env' 中移除")
return True, message
# 如果是列表、字典或集合类型将其转换为JSON字符串
if isinstance(converted_value, (list, dict, set)):
value_to_write = json.dumps(converted_value)
else:
value_to_write = (
str(converted_value) if converted_value is not None else ""
)
value_to_write = str(converted_value)
set_key(
dotenv_path=SystemUtils.get_env_path(),
@@ -967,7 +985,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
@property
def PROXY(self):
if self.PROXY_HOST:
if self.PROXY_HOST and self.PROXY_HOST.strip():
return {
"http": self.PROXY_HOST,
"https": self.PROXY_HOST,
@@ -1009,7 +1027,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
@property
def PROXY_SERVER(self):
if self.PROXY_HOST:
if self.PROXY_HOST and self.PROXY_HOST.strip():
try:
parsed = urlparse(self.PROXY_HOST)
if not parsed.scheme:

View File

@@ -92,6 +92,9 @@ def _get_shared_async_transport(
会话级状态由调用方在外层 AsyncClient(transport=...) 实例化时单独配置,
每次调用用完即销毁,因此天然无 jar 累积串扰。
"""
# 规范化代理:拒绝空字符串等非法值,防止 httpx 抛出 Unknown scheme for proxy URL
if proxy is not None and (not proxy or not proxy.strip()):
proxy = None
try:
loop = asyncio.get_running_loop()
except RuntimeError:
@@ -899,12 +902,17 @@ class AsyncRequestUtils:
# 如果已经是字符串格式,直接返回
if isinstance(proxies, str):
return proxies
return proxies.strip() or None
# 如果是字典格式提取http或https代理
if isinstance(proxies, dict):
# 优先使用https代理如果没有则使用http代理
proxy_url = proxies.get("https") or proxies.get("http")
# 先各自 strip避免空白字符串阻断裂合取或回退到 http 代理
https_proxy = proxies.get("https")
http_proxy = proxies.get("http")
https_proxy = https_proxy.strip() if isinstance(https_proxy, str) else None
http_proxy = http_proxy.strip() if isinstance(http_proxy, str) else None
proxy_url = https_proxy or http_proxy
if proxy_url:
return proxy_url