> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and Retries

> The SDK error hierarchy, and the rule that decides which failed requests it repeats

## Errors

Every error the SDK raises for the platform derives from `blobhub.BlobHubError`, and every class is importable from
`blobhub` and from `blobhub.errors`:

```python theme={null}
import blobhub

try:
    blob = hub.blob("acme", "checkout")
except blobhub.PermissionDenied as error:
    print(error.error, error.status, error.message)   # forbidden 403 ...
```

| Attribute | Holds |
| :- | :- |
| `error` | The machine-readable code: the server's `error`, or one the SDK derives from the HTTP status. |
| `message` | A human-readable message. |
| `status` | The HTTP status, or `None` when no response was involved. |
| `retry_after` | Seconds a `Retry-After` header asked for, or `None`. |
| `details` | The failure envelope's `details` list, or `None`. A graph query's OrientDB reason is here. |

`str(error)` is `<message> (<error>, HTTP <status>)`, or `<message> (<error>)` with no status. Match on the class
and on `error`, never on the message text.

| Class | Raised for | Typical `error` |
| :- | :- | :- |
| `AuthError` | HTTP 401: the credential was refused | `unauthorized` |
| `PermissionDenied` | HTTP 403, which the platform also answers for many missing resources | `forbidden` |
| `NotFound` | HTTP 404, or an artifact the revision does not hold yet | `artifact_not_found` |
| `Conflict` | HTTP 409; `error` names the state | `revision_not_writable`, `session_not_open` |
| `RateLimited` | HTTP 429: throttled before the request ran | `too_many_requests` |
| `ServerError` | HTTP 5xx, a body that is not JSON, or a response the SDK cannot use | `invalid_response` |
| `NetworkError` | A transport failure; the request may or may not have run | `network_error` |
| `RequestNotSent` | A subclass of `NetworkError`: the connection was never made | `request_not_sent` |
| `CommandError` | A failure envelope under any other status, usually 400 | `invalid_request_body` |
| `ConfigError` | The credentials file or the named profile cannot be used | `profile_not_found` |
| `TypeMismatch` | A typed module built over a revision of another blob type | `type_mismatch` |
| `OperationFailed` | An operation or a revision reached `failed` | `upload_failed` |
| `WaitTimeout` | A wait ran out of time; the work keeps going on the platform | `wait_timeout` |

`WaitTimeout` is also a `TimeoutError`. Problems with the arguments you pass, found before any request, raise
built-in exceptions instead: `ValueError`, `TypeError`, `FileNotFoundError`, and `FileExistsError` when a notebook
copy would overwrite a file.

**A 403 often means "absent".** The platform answers the same 403 for a target that does not exist and for one your
credential cannot reach, so it cannot tell you which. When a client fell back to an anonymous token because it found
no key, an `AuthError` or `PermissionDenied` says so in its message.

## Retries

A request that fails transiently may be repeated, but only when repeating it is safe:

| Request | Retried after |
| :- | :- |
| `GET`, a `/data/query` read, a presigned part transfer | 429, 5xx, any network failure, a body that is not JSON |
| Everything else: a `/data/command` write, any other `POST`, `PATCH` or `DELETE` | 429, and `RequestNotSent` only |

* Three attempts in all. Before the second the SDK waits a random time of up to 0.5 s, and before the third up to
  1 s.
* A `Retry-After` header replaces that wait, up to 60 s.
* `AuthError`, `PermissionDenied`, `NotFound`, `Conflict` and `CommandError` are never retried.
* Each API request times out after 30 s, and each presigned part transfer after 300 s.

When the attempts run out, the last error is raised.

### Why a write is not retried

A write can fail after the platform has carried it out. API Gateway answers 504 at 29 seconds while the handler
behind it keeps running, and a read timeout or a dropped connection says nothing about what happened on the server.
The platform takes no idempotency key, so repeating such a write would create a second session, a second execution,
or a second graph vertex.

The two failures a write may retry prove that nothing ran: a 429 is API Gateway's throttling, refused before any
handler runs, and `RequestNotSent` means the connection was never made. blobhub-cli and blobhub-worker follow the
same rule.

So a `ServerError` or a `NetworkError` that is not a `RequestNotSent`, raised by a write, means **the outcome is
unknown**. Look before you repeat it: read the thing the write would have created, such as the session's executions
or the graph's vertices, and write again only if it is not there.

## Logging

The SDK logs to the `blobhub` logger at `DEBUG` only: the method, path and status of each attempt, and each retry's
wait. It never logs a header, a body, or a presigned URL's query string, so turning it on cannot leak a credential:

```python theme={null}
import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger("blobhub").setLevel(logging.DEBUG)
```

## See also

* [REST API conventions](/rest-api/conventions) — the response envelope the errors are read from.
* [Credentials and profiles](/sdk/credentials) — `ConfigError` in detail.
* [Workflow](/sdk/workflow) — what to do when creating an execution fails with an unknown outcome.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.