bug: Partial-failure (207) ingestion response retries the whole batch up to 3x; per-item error details only logged at DEBUG
維護者通常 1 天內回覆
評估
這個 Issue 還沒有評估資料。
描述
Describe the bug
LangfuseClient._process_response turns an ingestion 207 response that has a non-empty errors list into raise APIErrors([...]) (request.py#L91-L109).
APIErrors is not a subclass of APIError, so the "do not retry 4xx" check in the score ingestion consumer (isinstance(e, APIError) and 400 <= int(e.status) < 500, score_ingestion_consumer.py#L177-L192) never matches, and backoff.on_exception(Exception, max_tries=3) re-posts the whole batch.
Expected: when most events of a score ingestion batch were accepted (successes) and one was rejected with a per-item 4xx, the batch is not re-sent in full, a permanent per-item 4xx is not retried, and the failing event id/status/message is logged.
Actual: the full batch (including events the server already accepted) is POSTed 3 times, and the only thing logged at ERROR level is the generic canned text for the status code ("Bad request. Please check your request ..."), without the item id or the server's message. The server message is only visible at DEBUG level (parse_error.py#L75-L99).
Impact is modest: duplicate delivery is likely absorbed by server-side idempotency on event id (I have not verified this against a server), but it wastes traffic, triples the time the consumer thread spends on the batch (which flush() waits for), and retries errors that can never succeed.
Steps to reproduce
Uses httpx.MockTransport, no server needed (run from a checkout with the repo on PYTHONPATH):
import httpx, json
from queue import Queue
from langfuse._utils.request import LangfuseClient
from langfuse._task_manager.score_ingestion_consumer import ScoreIngestionConsumer
import time; time.sleep = lambda s: None # skip backoff waits
calls = []
body = {"successes": [{"id": "e0", "status": 201}, {"id": "e2", "status": 201}],
"errors": [{"id": "e1", "status": 400, "message": "Invalid request data", "error": "bad"}]}
def h(req):
calls.append(json.loads(req.content)["batch"]); return httpx.Response(207, json=body)
cl = LangfuseClient("pk", "sk", "http://x", "1", 5, httpx.Client(transport=httpx.MockTransport(h)))
q = Queue(); c = ScoreIngestionConsumer(ingestion_queue=q, identifier=0, client=cl, public_key="pk", flush_at=3)
for i in range(3): q.put({"id": f"e{i}", "type": "score-create", "body": {"n": i}})
c.upload()
print("POST count:", len(calls), "ids per post:", [[e["id"] for e in b] for b in calls])
Observed output:
API errors occurred: Bad request. Please check your request for any missing or incorrect parameters. Refer to our API docs: https://api.reference.langfuse.com for details.
POST count: 3 ids per post: [['e0', 'e1', 'e2'], ['e0', 'e1', 'e2'], ['e0', 'e1', 'e2']]
Variant (re-run on the same commit): if the 207 body is malformed ({"errors": ["boom"]} or a JSON list), the .get calls in the 207 branch raise AttributeError (only JSONDecodeError is caught there), and that is also retried 3 times (3 POSTs observed).
Langfuse Cloud or self-hosted?
Not server dependent (reproduced with a mock transport). The 207 shape is the one returned by /api/public/ingestion.
If self-hosted, what version are you running?
n/a
SDK and integration versions
langfuse 4.17.0 (main @ bf11ec121479145923ff27b32ec2ad1d06fd2d51), Python 3.14.0, httpx and backoff as pinned in uv.lock.
Additional information
Possible direction: handle APIErrors separately in _upload_batch: log each item (id, status, message) at warning/error level, treat per-item 4xx other than 429/408 as final, and optionally re-send only the items that failed with 429/5xx. A smaller change is to not retry an APIErrors whose statuses are all non-retryable 4xx. The 207 branch could also check isinstance(payload, dict) and the entries before calling .get.
Related but different: #1874 (log non-retryable 4xx batch drops) covers a plain 4xx response, not the 207 path.
Are you interested in contributing a fix for this bug?
Yes
- 主要語言
- Python
- 星號
- 504
- 分支
- 391
- 平均合併
- 1 天 3 小時
- 30 天內合併 PR
- 74
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
langfuse/langfuse-python 的其他 Issue
-
bug(scores): create_score sends numpy.bool_ as JSON `true` and Decimal as "<Decimal>" since v5.0.2可能已有人在做 @lalith0407 於 9 天前認領。 未關閉bug feat-scores sdk-python
難度 2/5 1-3 小時 新手友好度 74/100
langfuse/langfuse-python#2057 ·
維護者通常 1 天內回覆
-
tests/unit/test_prompt.py fails on a fresh clone without LANGFUSE_* env vars可能已有人在做 @Mathoholic 於 23 天前認領。 未關閉bug feat-prompt-management sdk-python
難度 2/5 1-3 小時 新手友好度 80/100
langfuse/langfuse-python#1976 · 3 則留言 ·
維護者通常 1 天內回覆
-
bug(openai): a stream that fails mid-iteration ends the generation without level ERROR可能已有人在做 @hassiebp 於 12 天前認領。 未關閉bug integration-openai sdk-python
langfuse/langfuse-python#2039 · 已指派 1 人 ·
維護者通常 1 天內回覆
-
fix(serializer): handle single-string __slots__ and detect cycles in slotted objects可能已有人在做 @hassiebp 於 18 天前認領。 未關閉bug sdk-python
langfuse/langfuse-python#2014 · 已指派 1 人 ·
維護者通常 1 天內回覆
-
bug: ChatPromptClient.compile appends str(whole list) once per non-dict placeholder item可能已有人在做 @nimarb 於 13 天前認領。 未關閉bug feat-prompt-management sdk-python
langfuse/langfuse-python#1971 · 已指派 1 人 ·
維護者通常 1 天內回覆
查看 langfuse/langfuse-python 的全部 Issue
相似的 Issue
-
ci-rerun: collect.py fails a whole pr-babysit cycle on one transient GitHub 5xx and never retries未關閉follow-up
難度 2/5 1-3 小時 新手友好度 62/100
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 70/100
-
Built-in QUBO fallback applies bitwise xor to floating binary states可能已有人在做 @iloveeyjafjalla 今天認領。 未關閉
難度 2/5 1-3 小時 新手友好度 72/100
qboson/kaiwu-pytorch-plugin#1702 ·
維護者通常 3 天內回覆
-
bug v2
難度 2/5 1-3 小時 新手友好度 76/100
modelcontextprotocol/python-sdk#3811 ·
維護者通常 1 天內回覆
-
cost:cheap practicality:seen-in-the-wild severity:medium
難度 2/5 1-3 小時 新手友好度 72/100
fairagro/m4.2_advanced_middleware_api#620 ·
維護者通常 1 天內回覆