Skip to main content

Errors and retries

The exception hierarchy​

Every error the SDK raises for a platform response is a subclass of HoneyframeError:

HoneyframeError
├── HoneyframeConfigError missing or unsafe setting; raised before any request
└── HoneyframeAPIError any non-2xx response
├── HoneyframeAuthError 401, 403
├── HoneyframeNotFoundError 404
├── HoneyframeValidationError 400, 409, 422
├── HoneyframeRateLimitError 429 — retrying is correct (0.1.8+)
└── HoneyframeBudgetExceededError 429 budget_exceeded — not retryable (0.1.8+)

All of them are importable from honeyframeapi. A HoneyframeAPIError carries:

AttributeContent
status_codeThe HTTP status.
detailThe server's message: the detail field of the JSON body when there is one, otherwise the body text. Capped at 4 KB.
method, urlThe request that failed.
responseA copy of the httpx.Response for inspection. Its .request carries no credentials and no body.
from honeyframeapi import (
HoneyframeAuthError, HoneyframeNotFoundError, HoneyframeValidationError,
)

try:
ds = project.get_dataset("fact_orders")
ds.get_metadata()
except HoneyframeNotFoundError:
print("no such dataset in this project")
except HoneyframeAuthError as exc:
print("token expired, revoked or not allowed:", exc.detail)
except HoneyframeValidationError as exc:
print("refused:", exc.status_code, exc.detail)

A few other exceptions are worth knowing:

  • ValueError — an argument the SDK can check locally is out of range (an expires_days outside the allowed values, a limit above 200, an unknown statistics card type, an inconsistent chrome(...) combination).
  • TimeoutError — a wait gave up: JobsAPI.wait(raise_on_timeout=True), trigger_and_wait, build_code_environment(wait=True). The work continues on the server.
  • httpx exceptions — a network failure (connection refused, DNS failure, read timeout) that persists after the automatic retries is raised as the underlying httpx.TransportError subclass, not as a HoneyframeError. Catch httpx.TransportError (or httpx.HTTPError) if your script should survive network outages.

Rate limits and spent budgets​

Both arrive as HTTP 429, and they need opposite handling. The SDK tells them apart (0.1.8+; 0.1.7 raises a plain HoneyframeAPIError for any 429):

  • HoneyframeRateLimitError — a rate or concurrency limit. Waiting and retrying is correct. exc.retry_after holds the seconds from the Retry-After header, or None when the header is missing or uses the date form; fall back to your own backoff then.
  • HoneyframeBudgetExceededError — the organization's metered budget is spent. It does not clear on a timer (the quota resets with the billing month), so retrying cannot succeed. Surface it to a person: raise the budget, or wait for the reset.

The SDK never retries a 429 itself, because only you know whether waiting inside your process is acceptable:

import time
from honeyframeapi import HoneyframeBudgetExceededError, HoneyframeRateLimitError

def with_rate_limit_retry(call, attempts=5):
for attempt in range(attempts):
try:
return call()
except HoneyframeBudgetExceededError:
raise # never retry a spent budget
except HoneyframeRateLimitError as exc:
if attempt == attempts - 1:
raise
time.sleep(exc.retry_after if exc.retry_after is not None else 2 ** attempt)

reply = with_rate_limit_retry(lambda: client.get_agent(42).ask("Orders today?"))

Automatic retries​

The client retries a request automatically only when all of these hold:

  • the method is GET, HEAD or OPTIONS (no side effects), and
  • the failure is a transport error (connection reset, read timeout, …) or a 502, 503 or 504 response — the kind of failure a restart or proxy hiccup causes.

Writes (POST, PUT, PATCH, DELETE) are never retried: a write that failed in transit may already have been applied, and repeating it could apply it twice. 429 responses are never retried (above).

The backoff doubles each time: retry_backoff, then 2 × retry_backoff, and so on.

client = HoneyframeClient.from_env(
max_retries=4, # default 2 — so up to 3 attempts in total
retry_backoff=1.0, # default 0.5 seconds
)
client = HoneyframeClient.from_env(max_retries=0) # no automatic retries

Expired sessions. Separately from the retries above, when a request returns 401 and the client signed in with a username and password, it signs in again and repeats the request once. A token passed as token= is not refreshed; the 401 is raised as HoneyframeAuthError.

Timeouts​

The client's timeout (default 60 seconds; None disables it) applies to each request. Calls that can take longer take their own timeout= in seconds:

Calltimeout=
Dashboard.export_pdf, Dashboard.execute_all, Dataset.materializeOverrides the client timeout for that request (0.1.8+).
client.request(...)Overrides the client timeout for that request (0.1.8+).
MlRecipe.run, PluginRecipe.runBounds the synchronous run's HTTP call (0.1.8+).
Scenario.run_and_wait, JobsAPI.wait, trigger_and_wait, Project.run_dbt(wait=True), Recipe.build(wait=True)How long to keep polling; each poll is a normal request.

Synchronous dbt calls through project.dbt (run, test, build, execute) use the client timeout; raise it for long builds, for example HoneyframeClient.from_env(timeout=1800).

When every call fails​

If a script that worked suddenly fails on every call with HoneyframeAuthError, check the organization's license before the token: client.license_status() (0.1.8+) answers even when the platform is locked. Only valid and grace let other requests through; every status except valid comes with a message.

print(client.license_status()["status"])

Verified against honeyframeapi 0.1.8.