fix: 重试临时外部 API 错误
This commit is contained in:
+26
-5
@@ -94,15 +94,36 @@ class TikHubClient:
|
|||||||
status_code=response.status_code,
|
status_code=response.status_code,
|
||||||
)
|
)
|
||||||
if response.is_error:
|
if response.is_error:
|
||||||
raise PlatformAPIError(
|
if attempt >= self.max_retries:
|
||||||
f"External API returned HTTP {response.status_code}",
|
raise PlatformAPIError(
|
||||||
error_type="api_error",
|
self._format_api_error(response),
|
||||||
status_code=response.status_code,
|
error_type="api_error",
|
||||||
)
|
status_code=response.status_code,
|
||||||
|
)
|
||||||
|
time.sleep(2**attempt)
|
||||||
|
continue
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
raise PlatformAPIError("External API request failed", error_type="api_error", status_code=last_status)
|
raise PlatformAPIError("External API request failed", error_type="api_error", status_code=last_status)
|
||||||
|
|
||||||
|
def _format_api_error(self, response: httpx.Response) -> str:
|
||||||
|
message = f"External API returned HTTP {response.status_code}"
|
||||||
|
detail = self._response_error_detail(response)
|
||||||
|
return f"{message}: {detail}" if detail else message
|
||||||
|
|
||||||
|
def _response_error_detail(self, response: httpx.Response) -> str | None:
|
||||||
|
try:
|
||||||
|
payload = response.json()
|
||||||
|
except ValueError:
|
||||||
|
text = response.text.strip()
|
||||||
|
return text[:200] if text else None
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
for key in ("message_zh", "message", "error", "detail"):
|
||||||
|
value = payload.get(key)
|
||||||
|
if value:
|
||||||
|
return str(value)[:200]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_timestamp(value: Any) -> datetime | None:
|
def parse_timestamp(value: Any) -> datetime | None:
|
||||||
if value in (None, ""):
|
if value in (None, ""):
|
||||||
|
|||||||
@@ -53,6 +53,61 @@ def test_tikhub_client_raises_structured_error_after_retries(monkeypatch):
|
|||||||
assert sleeps == [1, 2, 4]
|
assert sleeps == [1, 2, 4]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tikhub_client_retries_transient_api_error(monkeypatch):
|
||||||
|
sleeps = []
|
||||||
|
monkeypatch.setattr("app.platforms.base.time.sleep", sleeps.append)
|
||||||
|
transport = SequenceTransport(
|
||||||
|
[
|
||||||
|
httpx.Response(400, json={"message_zh": "临时请求失败"}),
|
||||||
|
httpx.Response(200, json={"ok": True}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
http_client = httpx.Client(transport=httpx.MockTransport(transport))
|
||||||
|
client = TikHubClient(base_url="https://api.test", api_key="secret-token", http_client=http_client)
|
||||||
|
|
||||||
|
result = client.get("/demo")
|
||||||
|
|
||||||
|
assert result == {"ok": True}
|
||||||
|
assert sleeps == [1]
|
||||||
|
assert len(transport.requests) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_tikhub_client_includes_response_summary_after_api_error_retries(monkeypatch):
|
||||||
|
sleeps = []
|
||||||
|
monkeypatch.setattr("app.platforms.base.time.sleep", sleeps.append)
|
||||||
|
transport = SequenceTransport(
|
||||||
|
[httpx.Response(400, json={"message_zh": "笔记评论暂不可用"}) for _ in range(4)]
|
||||||
|
)
|
||||||
|
http_client = httpx.Client(transport=httpx.MockTransport(transport))
|
||||||
|
client = TikHubClient(base_url="https://api.test", api_key="secret-token", http_client=http_client)
|
||||||
|
|
||||||
|
with pytest.raises(PlatformAPIError) as exc_info:
|
||||||
|
client.get("/demo")
|
||||||
|
|
||||||
|
assert exc_info.value.error_type == "api_error"
|
||||||
|
assert exc_info.value.status_code == 400
|
||||||
|
assert "External API returned HTTP 400" in str(exc_info.value)
|
||||||
|
assert "笔记评论暂不可用" in str(exc_info.value)
|
||||||
|
assert "secret-token" not in str(exc_info.value)
|
||||||
|
assert sleeps == [1, 2, 4]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tikhub_client_includes_text_response_summary_after_api_error_retries(monkeypatch):
|
||||||
|
sleeps = []
|
||||||
|
monkeypatch.setattr("app.platforms.base.time.sleep", sleeps.append)
|
||||||
|
transport = SequenceTransport([httpx.Response(502, text="upstream temporary failure") for _ in range(4)])
|
||||||
|
http_client = httpx.Client(transport=httpx.MockTransport(transport))
|
||||||
|
client = TikHubClient(base_url="https://api.test", api_key="secret-token", http_client=http_client)
|
||||||
|
|
||||||
|
with pytest.raises(PlatformAPIError) as exc_info:
|
||||||
|
client.get("/demo")
|
||||||
|
|
||||||
|
assert exc_info.value.error_type == "api_error"
|
||||||
|
assert exc_info.value.status_code == 502
|
||||||
|
assert str(exc_info.value) == "External API returned HTTP 502: upstream temporary failure"
|
||||||
|
assert sleeps == [1, 2, 4]
|
||||||
|
|
||||||
|
|
||||||
def test_tikhub_client_reports_401_as_auth_error_without_leaking_token():
|
def test_tikhub_client_reports_401_as_auth_error_without_leaking_token():
|
||||||
transport = SequenceTransport([httpx.Response(401, json={"message": "Unauthorized"})])
|
transport = SequenceTransport([httpx.Response(401, json={"message": "Unauthorized"})])
|
||||||
http_client = httpx.Client(transport=httpx.MockTransport(transport))
|
http_client = httpx.Client(transport=httpx.MockTransport(transport))
|
||||||
|
|||||||
Reference in New Issue
Block a user