Skip to content

fix(byoc): signer error bodies must never mask the status code - #66

Open
seanhanca wants to merge 1 commit into
mainfrom
fix/signer-error-body-read
Open

fix(byoc): signer error bodies must never mask the status code#66
seanhanca wants to merge 1 commit into
mainfrom
fix/signer-error-body-read

Conversation

@seanhanca

Copy link
Copy Markdown
Contributor

Problem

Diagnosing failed creative job cjob_0a9056941b2a (2026-08-21): when the signer rejects a dead Daydream key at /generate-live-payment with HTTP 401 (193-byte AUTH/FAILED JSON body), the SDK surfaces:

No orchestrator available for capability 'X': payment failed: IncompleteRead(84 bytes read, 109 more expected)

84 + 109 = 193 — the signer's exact body size. Mechanism (reproduced): the signer answers 401 before consuming the multi-KB POST body (base64 OrchestratorInfo) and closes the connection; the reset can truncate the client's buffered response, so e.read() inside the except HTTPError handler raises http.client.IncompleteRead, which escapes the handler — the 401 status never reaches the message. (With a very large body the same race lands as an SSL EOF during upload; at real payload sizes it lands as this truncated read.)

Downstream, payment failed: IncompleteRead(...) classified as "GPU network briefly busy — retry" — the opposite of a permanent per-key auth failure.

Fix

  • _read_http_error_body(e, limit) — defensive body read: salvages IncompleteRead.partial (the first 84 bytes contain "Authentication failed","code":"AUTH/FAILED" — plenty), never raises. Applied at all five HTTPError sites: the two signer paths (_create_byoc_payment, _sign_byoc_job) were fully naked; the three orch paths kept the status but dropped the body on truncation.
  • Status-first signer messages — a 401 now reports signer rejected key: HTTP 401: <body> so error classifiers can distinguish a dead key from an outage. 403 keeps its existing message shape (BYOC payment generation failed: HTTP 403: ...) — the storyboard-side out-of-credits classifier matches on it.

Verification

  • tests/test_http_error_body.py: 7 new tests — helper salvage/never-raise/limit, plus end-to-end through _sign_byoc_job's real except-handler with a body whose read raises IncompleteRead mid-flight (asserts signer rejected key + HTTP 401 in the message, and that 403/500 shapes are unchanged).
  • Full suite: 251 passed.

Companion PR: livepeer/storyboard#1102 maps both the legacy IncompleteRead mask and this new explicit shape to payment_unauthorized (non-retryable).

🤖 Generated with Claude Code

When the signer rejects a dead API key at /generate-live-payment with
401, it answers before consuming the multi-KB POST body (base64
OrchestratorInfo) and closes the connection. The client's buffered
response can then be truncated, so e.read() inside the except-HTTPError
handler raises http.client.IncompleteRead — which escapes the handler
and surfaces as 'payment failed: IncompleteRead(84 bytes read, 109 more
expected)' with no trace of the 401. Downstream classifiers read that
as a transient GPU outage and users burn retries against a permanently
dead key (live incident 2026-08-21, cjob_0a9056941b2a; 84+109 = the
signer's exact 193-byte AUTH/FAILED body).

- New _read_http_error_body(): reads the body defensively, salvaging
  IncompleteRead.partial, never raising. Used at all five HTTPError
  sites (the two signer paths were fully naked; the three orch paths
  kept the status but dropped the body on a truncated read).
- The signer paths now lead with the status and name the failure:
  'signer rejected key: HTTP 401: <body>'. 403 keeps its existing
  message shape — downstream out-of-credits classifiers match on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seanhanca
seanhanca requested a review from j0sh as a code owner August 21, 2026 03:38
"""

import io
import json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant