Repository navigation
feat(client)!: typed HTTP errors, a configurable timeout, opt-in GET retries and faucet TryLater - #35
Merged
Merged
Conversation
…ET retries A response with a status other than 200 raises an ApiError whose message is the `message` field of the node's JSON error body, falling back to the body and then to the HTTP reason phrase. The error also keeps the response headers and body. 404, 408, 410, 429 and 503 raise subclasses of ApiError: ApiNotFoundError, ApiRequestTimeoutError, ApiGoneError (with oldest_block, from the x-supra-oldest-block header), ApiRateLimitError (with retry_after_seconds, from the body's retry_after_seconds or the Retry-After header) and ApiServiceUnavailableError. ApiClientConfig gains two keyword-only fields. `timeout` (seconds, default 60) limits connecting, reading, writing and waiting for a pooled connection; the pool wait had no limit before. `retry` takes a RetryPolicy that repeats GET requests after a 408, 429 or 503 response or a timeout, up to a bounded number of attempts, waiting for the time the response asks for (up to a cap) or an exponential backoff. It defaults to None, so no request is repeated unless the caller sets it. POST requests are never repeated. A request that exceeds the timeout raises ClientTimeoutError, a subclass of httpx.TimeoutException. A view response without `result` raises UnexpectedResponseError, which names the field and subclasses both ApiError and KeyError. The new classes and RetryPolicy are exported from supra_sdk.clients. BREAKING CHANGE: ApiError.message is the node's error message instead of the whole response body; the body is ApiError.body. A request timeout raises ClientTimeoutError instead of the specific httpx timeout class (ReadTimeout, ConnectTimeout, ...); `except httpx.TimeoutException` still matches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… request The node answers a faucet request that no faucet account is free to fund with 200 and the JSON string "TryLater". faucet() read it as an object and raised AttributeError. It now returns None for it, so faucet(..., wait_for_faucet=False) returns None and faucet(...) raises FaucetRequestNotAcceptedError, as documented. A 200 body that is neither a JSON object nor "TryLater" raises UnexpectedResponseError. The faucet request is sent once even when ApiClientConfig.retry is set, because each accepted request funds the account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly and reject malformed faucet answers ClientTimeoutError carries the request of the httpx timeout it replaces, so `error.request` works as it did for httpx.ReadTimeout, and it survives pickling. Parsing an error response raises nothing but the ApiError: a body nested beyond the recursion limit is kept as text, and a retry_after_seconds, Retry-After or x-supra-oldest-block value out of the float or int range is ignored. A timeout outside the float range is a ValueError. The backoff exponent is capped, so a large RetryPolicy.attempts cannot overflow. faucet() raises UnexpectedResponseError for a 200 body that is not JSON, and for an object whose Accepted is missing or not a non-empty string, where it returned None before. None now means only "TryLater". The ApiNotFoundError, ApiError and README texts now say that v11.7 nodes answer a missing resource with 404, and that only view() and faucet() raise UnexpectedResponseError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # README.md
…faucet errors - `wait_for_transaction` and `faucet()` treat a 404 whose body is nested deeper than the interpreter's recursion limit as an ordinary 404 and raise it, instead of failing with a bare RecursionError. - `UnexpectedResponseError` takes an optional `problem` that completes its message. A faucet 200 whose `Accepted` is empty or not a string says so, a body that is not JSON says so, and a missing field keeps the existing text. - README describes the faucet outcomes: `"TryLater"`, non-JSON bodies and an `Accepted` that is missing, empty or not a string. - `retry_after_seconds` documents the string forms it accepts. - Tests cover an out-of-range integer retry delay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
faucet()handles the node's"TryLater"answer instead of raisingAttributeError, and the faucet request isnever repeated.
ApiErrorstays the base class and gains the parsed node message, the raw body and theresponse headers; subclasses cover 404, 408, 410, 429 and 503.
view()andfaucet()raise an error that says what is wrong when a 200 response lacks a usable field they read(
result,Accepted).Public API
Exported from
supra_sdk.clients:ApiError(message, status_code)unchanged. Addsheaders,body;messageis the JSONmessagefield, falling back to the body textApiNotFoundErrorApiRequestTimeoutErrorApiGoneErroroldest_block: int | Nonefrom thex-supra-oldest-blockheader, the oldest block the node still holdsApiRateLimitErrorretry_after_seconds: float | Nonefrom the body'sretry_after_secondsor theRetry-Afterheader (seconds or HTTP date)ApiServiceUnavailableErrorUnexpectedResponseErrorApiErrorandKeyError;fieldnames the field, and the message says whether it is missing, unusable, or the body is not JSON. Raised byview()andfaucet()ClientTimeoutErrorhttpx.TimeoutException; keeps the request;url,timeout_seconds,phaseRetryPolicyattempts=3,base_delay_seconds=0.25,max_delay_seconds=10.0ApiClientConfig(andSupraClientConfig) gain the keyword-only fieldstimeout: float = 60.0andretry: RetryPolicy | None = None; their positional order is unchanged.Behaviour
"TryLater"(no faucet account was free; it does not count against the address) makesfaucet(..., wait_for_faucet=False)returnNoneandfaucet(...)raiseFaucetRequestNotAcceptedError, as documented.Acceptedis not a non-empty string, raisesUnexpectedResponseError.retrysays, because each accepted request funds the account.RetryPolicy, a GET is repeated after 408, 429, 503 or a timeout, up toattemptsin total. It waits forwhat the server asks (body or
Retry-After), and raises at once if the server asks for longer thanmax_delay_seconds. Otherwise it backs off exponentially.timeoutseconds. The pool timeout was unlimited.ApiError.messageis now the node's message, not the whole response body; the body isApiError.body.str(error)keeps its format and still contains the node's message.ClientTimeoutErrorinstead of the specifichttpxtimeout class;except httpx.TimeoutExceptionstill catches it.{}or whoseAcceptedis empty or not a string now raisesUnexpectedResponseError. Before,faucet(..., wait_for_faucet=False)returnedNone,""or the value asit was, and
faucet(...)raisedFaucetRequestNotAcceptedError; a caller catching that error for these bodiesno longer catches them.
json.JSONDecodeError, aValueError. It now raisesUnexpectedResponseError, whichexcept ValueErrordoes not catch.view()withoutresultraisesUnexpectedResponseError, which is still aKeyError. A non-JSON 200 fromview()used to raisejson.JSONDecodeErrorand now raisesUnexpectedResponseError.ApiErrorit builds (non-JSON, deeply nested, orout-of-range values are kept as text or ignored). This includes the 404 check made while
wait_for_transactionand
faucet()wait.Checks
make fmt-check,make lint,make typecheck: clean.masterand the review fixes:make test-unit: 372 tests, OK (26 node-backed tests skipped).tests/unit/test_api_errors.pyandtests/unit/test_faucet.pyrun overhttpx.MockTransport. They cover:"TryLater"with bothwait_for_faucetvalues, and the malformed faucet bodies;retry_after_secondsfrom the body, from the header and from an HTTP date;ApiClientConfig(timeout=...)setting the request and pool timeouts;x-supra-oldest-block;.requestofClientTimeoutError.make test-spec: 248 scenarios passed.make build: sdist and wheel built.supra_node_v12.0.0.rc7and asupra_node_v11.7.2localnet, before the merge withmaster: every test passed except the ones then failing onmasterfor reasons outside this change, which test: run the node-backed tests and the automation example on v11.7 and v12 nodes #37fixed.
🤖 Generated with Claude Code