mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(headers): 兼容非 ASCII header 值,避免 httpx ASCII 编码报错
将 HeaderBuilder 中的 latin-1 透传方案替换为 ASCII 归一化策略: - 对 x-codex-turn-metadata 做 JSON 重编码(ensure_ascii=true)保留语义 - 其他非 ASCII 头值仅转义非 ASCII 字符为 \uXXXX,保留 ASCII 字符原样 - 新增单测覆盖中文 header 与 codex turn metadata 场景 Close #180 Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Set as AbstractSet
|
||||
from typing import Any
|
||||
|
||||
@@ -365,27 +366,48 @@ class HeaderBuilder:
|
||||
return self
|
||||
|
||||
def build(self) -> dict[str, str]:
|
||||
"""构建最终的头部字典
|
||||
|
||||
httpx 要求 header 值可被 latin-1 编码。对于包含非 latin-1 字符
|
||||
(如中文)的值,先 UTF-8 编码再按 latin-1 解码,使 httpx 将原始
|
||||
UTF-8 字节逐字节发送到上游 —— 与 Go net/http 的行为一致。
|
||||
"""
|
||||
"""构建最终的头部字典"""
|
||||
result: dict[str, str] = {}
|
||||
for original_key, value in self._headers.values():
|
||||
try:
|
||||
value.encode("latin-1")
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
# 将 UTF-8 字节逐字节映射为 latin-1 字符串,httpx 会原样发送
|
||||
logger.debug(
|
||||
"Header '{}' contains non-latin-1 chars, encoding as raw UTF-8 bytes",
|
||||
original_key,
|
||||
)
|
||||
value = value.encode("utf-8").decode("latin-1")
|
||||
result[original_key] = value
|
||||
result[original_key] = _normalize_header_value_for_httpx(original_key, value)
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_header_value_for_httpx(key: str, value: str) -> str:
|
||||
"""将 header 值归一化为 httpx/h11 可发送的 ASCII 字符串。
|
||||
|
||||
说明:
|
||||
- 当前 httpx/h11 栈会对 str 类型 header 值执行 ASCII 编码。
|
||||
- 若值包含非 ASCII 字符(如中文),会抛出 UnicodeEncodeError。
|
||||
- 因此这里统一做 ASCII 归一化,确保请求可稳定发出。
|
||||
"""
|
||||
if value.isascii():
|
||||
return value
|
||||
|
||||
key_lower = key.lower()
|
||||
|
||||
# Codex CLI 元数据是 JSON 字符串,优先重编码为 ASCII JSON,语义最稳定。
|
||||
if key_lower == "x-codex-turn-metadata":
|
||||
try:
|
||||
normalized = json.dumps(json.loads(value), ensure_ascii=True, separators=(",", ":"))
|
||||
logger.debug(
|
||||
"Header '{}' contains non-ASCII chars, normalized as ASCII JSON",
|
||||
key,
|
||||
)
|
||||
return normalized
|
||||
except Exception:
|
||||
# 非法 JSON 时走通用兜底,避免阻断请求。
|
||||
pass
|
||||
|
||||
# 兜底:仅将非 ASCII 字符替换为 \uXXXX,保留 ASCII 字符原样
|
||||
escaped = "".join(c if c.isascii() else f"\\u{ord(c):04x}" for c in value)
|
||||
logger.warning(
|
||||
"Header '{}' contains non-ASCII chars, escaped for httpx compatibility",
|
||||
key,
|
||||
)
|
||||
return escaped
|
||||
|
||||
|
||||
def build_upstream_headers_for_endpoint(
|
||||
original_headers: dict[str, str],
|
||||
endpoint: str | EndpointSignature | tuple,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from src.core.api_format import (
|
||||
CORE_REDACT_HEADERS,
|
||||
HeaderBuilder,
|
||||
@@ -73,6 +75,23 @@ class TestHeaderBuilder:
|
||||
assert built["Authorization"] == "base"
|
||||
assert built["X-Test"] == "1"
|
||||
|
||||
def test_non_ascii_value_is_escaped_for_httpx(self) -> None:
|
||||
builder = HeaderBuilder()
|
||||
builder.add("X-Test", "桌面")
|
||||
built = builder.build()
|
||||
assert built["X-Test"] == r"\u684c\u9762"
|
||||
|
||||
def test_codex_turn_metadata_is_normalized_to_ascii_json(self) -> None:
|
||||
builder = HeaderBuilder()
|
||||
raw = '{"turn_id":"t1","workspaces":{"d:\\\\桌面\\\\123\\\\Aether":{"has_changes":true}}}'
|
||||
builder.add("x-codex-turn-metadata", raw)
|
||||
built = builder.build()
|
||||
|
||||
normalized = built["x-codex-turn-metadata"]
|
||||
assert normalized.isascii()
|
||||
parsed = json.loads(normalized)
|
||||
assert "d:\\桌面\\123\\Aether" in parsed["workspaces"]
|
||||
|
||||
|
||||
class TestBuildUpstreamHeaders:
|
||||
def test_priority_and_drop_headers(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user