From 44d459fc50d357133b32517d1f7fdafe5e2bc326 Mon Sep 17 00:00:00 2001 From: meijiali <你的邮箱@xxx.com> Date: Mon, 6 Jul 2026 16:09:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E8=AF=95=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E5=A4=96=E9=83=A8=20API=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/platforms/base.py | 31 ++++++++++++--- tests/unit/test_comment_pagination.py | 55 +++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/app/platforms/base.py b/app/platforms/base.py index 73d8528..2ffdafe 100644 --- a/app/platforms/base.py +++ b/app/platforms/base.py @@ -94,15 +94,36 @@ class TikHubClient: status_code=response.status_code, ) if response.is_error: - raise PlatformAPIError( - f"External API returned HTTP {response.status_code}", - error_type="api_error", - status_code=response.status_code, - ) + if attempt >= self.max_retries: + raise PlatformAPIError( + self._format_api_error(response), + error_type="api_error", + status_code=response.status_code, + ) + time.sleep(2**attempt) + continue return response.json() 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: if value in (None, ""): diff --git a/tests/unit/test_comment_pagination.py b/tests/unit/test_comment_pagination.py index f2df7b2..0dc8eb6 100644 --- a/tests/unit/test_comment_pagination.py +++ b/tests/unit/test_comment_pagination.py @@ -53,6 +53,61 @@ def test_tikhub_client_raises_structured_error_after_retries(monkeypatch): 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(): transport = SequenceTransport([httpx.Response(401, json={"message": "Unauthorized"})]) http_client = httpx.Client(transport=httpx.MockTransport(transport))