# Errors

Every error the SDK raises descends from `SahiBaseException`, so one `except`
clause can handle anything it might raise. Below that the tree splits three
ways, by where the problem is:

- **`SahiConfigurationException`**: the SDK was used wrongly before any request
  was made. A token of the wrong shape, a modify with nothing to change. Raised locally, never from a server reply.
- **`SahiAPIException`**: a request reached a Sahi host and came back non-2xx,
  or never came back. It carries the server's `status`, `code` and `message`,
  the `request_id` to quote to support, and a `hint` saying what to do.
- **`SahiFeedException`**: a WebSocket stream failed to connect, was closed by
  the server for good, or was asked for a value it does not hold.

**Catch by class, not by `code` or `message`.** The auth host (`apis.sahi.com`)
and the API host (`direct.sahi.com`) spell the same code differently
(`Unauthorized`, `UNAUTHORIZED`); the class is the same for both.

The SDK also raises three plain Python exceptions, before sending anything:

- `ValueError`: an empty id list, an unknown candle size, a
  `to_ms` before `from_ms`, or an unknown tick-stream mode.
- `TypeError`: a basket leg missing a required key, or carrying one the API does
  not define.
- `RuntimeError`: a call on a client after `close()`.

## The tree

- `SahiBaseException`
  - `SahiConfigurationException`
  - `SahiAPIException`
    - `SahiBadRequestException` (400)
      - `SahiValidationException` (`VALIDATION_FAILED`)
    - `SahiAuthenticationException` (401)
      - `SahiAccountLockedException` (`Locked`)
    - `SahiAuthorisationException` (403)
      - `SahiKeyNotApprovedException` (403 on the auth host)
      - `SahiIpNotAllowedException` (`IP_NOT_ALLOWED`)
    - `SahiNotFoundException` (404)
      - `SahiOrderNotFoundException` (`ORDER_NOT_FOUND`)
    - `SahiInvalidOrderStateException` (409 `INVALID_ORDER_STATE`)
    - `SahiRateLimitException` (429)
    - `SahiServerException` (5xx)
      - `SahiMaintenanceException` (503 `UNDER_MAINTENANCE`)
    - `SahiTimeoutException` (no reply in time)
    - `SahiConnectionException` (could not connect)
  - `SahiFeedException`
    - `SahiFeedConnectionException`
    - `SahiFeedAuthExpiredException`
    - `SahiFeedForbiddenException`
    - `SahiFeedNotSubscribedException`

`SahiKeyNotApprovedException` is a subclass of `SahiAuthorisationException`, so
an `except SahiAuthorisationException` around a mint and a call catches both:
catch `SahiKeyNotApprovedException` first if you handle them differently.

## How a reply becomes an exception

A known `code` decides the class whatever the status; otherwise the status and
the host do. Anything not in the table becomes `SahiServerException` for a 5xx
and `SahiAPIException` otherwise. A 4xx without the API's error envelope was
refused by the edge before it reached the API, usually for an over-long URL,
and is a plain `SahiAPIException`.

| Status | Host | Code | Class |
|---|---|---|---|
| any | any | `VALIDATION_FAILED` | `SahiValidationException` |
| any | any | `ORDER_NOT_FOUND` | `SahiOrderNotFoundException` |
| any | any | `INVALID_ORDER_STATE` | `SahiInvalidOrderStateException` |
| any | any | `IP_NOT_ALLOWED` | `SahiIpNotAllowedException` |
| any | any | `Locked` | `SahiAccountLockedException` |
| any | any | `UNDER_MAINTENANCE` | `SahiMaintenanceException` |
| `4xx` | edge | no envelope | `SahiAPIException` |
| `400` | any | other | `SahiBadRequestException` |
| `401` | any | other | `SahiAuthenticationException` |
| `403` | auth | other | `SahiKeyNotApprovedException` |
| `403` | API | other | `SahiAuthorisationException` |
| `404` | any | other | `SahiNotFoundException` |
| `429` | any | any | `SahiRateLimitException` |
| `5xx` | any | any | `SahiServerException` |

## What an API exception carries

| Attribute | Type | Meaning |
|---|---|---|
| `status` | int or None | The HTTP status, or `None` when no reply arrived. |
| `code` | str or None | The envelope's `code`, verbatim as the host sent it. |
| `message` | str or None | The server's message. |
| `request_id` | str or None | The id to quote to Sahi support: the server's when it returned one, otherwise the one the SDK sent. |
| `hint` | str | What to do about it, per class. |

`str(exc)` combines all of them, so logging the exception is enough to
diagnose and act on it:

```text
HTTP 403 IP_NOT_ALLOWED: order writes must come from a registered IP. Order writes must come from a static IP registered on your account. Check the public egress address of this host — it usually means the address changed. [request_id=sahiapi-5f1c...]
```

`SahiRateLimitException` carries one thing more: `retry_after`, seconds to
wait, from the `Retry-After` header, or `None` on the auth host, which sends
none; wait for the next minute there.

A feed exception carries `close_code` instead: the WebSocket close code the
server sent (`4401`, `4403` or `4429`; the `SahiAPI.WS_CLOSE_*` constants name
them), or `None` when there was none, such as a refused handshake.

## Retries

The SDK retries one case itself: a `GET` that gets `429` with a `Retry-After`
of five seconds or less is retried once, after that wait. Everything else is
raised for you to handle:

- a longer `Retry-After`, from a per-minute or per-day limit;
- any `POST`, the margin checks included;
- any order write, since a write that timed out may have reached the exchange.

## Catching by class

Catch the class you can act on and let the rest propagate. Around the mint:

```python
from sahiapi import SahiAPI, SahiAuthenticationException, SahiKeyNotApprovedException

try:
    api = SahiAPI.from_credentials(key_id="pk_...", secret="sahi_sk_...")
except SahiKeyNotApprovedException:
    print("approve the key for today in your Sahi account, then run again")
    raise
except SahiAuthenticationException as e:
    print("sign-in failed:", e)  # wrong secret, revoked key, locked account
    raise
```

Around an order write, wrap the `place_order` call of Getting started's
[Place and cancel an order](/sdk/python/#place-and-cancel-an-order) like this:

```python
from sahiapi import (
    SahiIpNotAllowedException,
    SahiRateLimitException,
    SahiTimeoutException,
    SahiValidationException,
)

try:
    result = api.place_order(...)  # the same arguments as in Getting started
except SahiIpNotAllowedException:
    print("this machine's public IP is not registered on the account")
except SahiValidationException as e:
    print("refused:", e.message)
except SahiRateLimitException as e:
    print("over the limit; wait", e.retry_after, "seconds")
except SahiTimeoutException:
    print("it may have gone through; check api.get_order_list() before resending")
```

For a stream, the exception comes out of `consume()`:

```python
from sahiapi import SahiFeed, SahiFeedAuthExpiredException

feed = SahiFeed(api)
feed.subscribe_ltp([instrument_id], on_data_received=lambda data, meta: print(data))
try:
    feed.consume()
except SahiFeedAuthExpiredException:
    print("the token expired at 06:00 IST; mint a new one and build a new feed")
```

## Every class

### SahiBaseException

Base class for everything the SDK raises.

### SahiConfigurationException

The SDK was misconfigured or misused before a request could be made: a
`SahiAPI()` or `set_access_token()` given something other than the token
response or its `access_token` string, a value passed to
`sahiapi.token_expires_at()` or `sahiapi.token_scopes()` that is not a JWT, or a
`modify_order` with nothing to change. The message says
what was wrong and what to pass instead.

### SahiAPIException

A request to a Sahi host failed. Usually a non-2xx reply; the two subclasses
`SahiConnectionException` and `SahiTimeoutException` cover the cases where no
reply arrived. Also raised directly for a failed scrip-master download, with
`status` set when the file host answered.

### SahiBadRequestException

`400`: the request was malformed. On the auth host this also covers `scopes`
that are not a subset of the key's.

### SahiValidationException

`VALIDATION_FAILED`, as `400` for a malformed field or `422` for one the API
rejects on meaning: an unknown symbol, a quantity over the freeze quantity, a
contract past expiry, a `REGULAR` order while the market is closed. The server
`message` names the field or rule.

### SahiAuthenticationException

`401`: the credential or token was missing, unknown, wrong, revoked or
expired. The auth host deliberately does not say which. When a `/v1` call
raises this, mint a new token.

### SahiAccountLockedException

`401 Locked`: the account behind the credential is frozen or locked. Contact
Sahi support.

### SahiAuthorisationException

`403` under `/v1`: the token is valid but lacks the scope the endpoint needs.
Scopes are fixed when the key is created and a token can only narrow them.

### SahiKeyNotApprovedException

`403` on the auth host: the key is not approved for the current trading day.
Approve it in your Sahi account and mint again.

### SahiIpNotAllowedException

`403 IP_NOT_ALLOWED`: an order write did not come from a registered IP. Reads
and market data are not IP-checked. It usually means this machine's public
address changed.

### SahiNotFoundException

`404`: the route or resource does not exist. Also returned by the funds,
margin and profile endpoints when the account behind the token is not yet set
up for trading.

### SahiOrderNotFoundException

`404 ORDER_NOT_FOUND`: no such order on today's book for this account.

### SahiInvalidOrderStateException

`409 INVALID_ORDER_STATE`: only `Open` and `OpenPartial` orders can be modified or
cancelled; a completed, cancelled or rejected order is final.

### SahiRateLimitException

`429`: a rate-limit window was exhausted. `retry_after` is the seconds the
window has left, or `None` on the auth host. Batch requests rather than looping
per instrument.

### SahiServerException

`5xx`: a failure on Sahi's side, usually `code` `INTERNAL`. Retry a read
after a short wait; check the order book before retrying an order write. If a
read keeps failing, write to [developers@sahi.com](mailto:developers@sahi.com)
with the `request_id`.

### SahiMaintenanceException

`503 UNDER_MAINTENANCE`: the margin checks are closed for the daily 04:45–07:00
IST start-of-day processing. A subclass of `SahiServerException`. Retry after
07:00 IST.

### SahiTimeoutException

No reply arrived within the timeout. The request may or may not have reached
the server. For an order write, look for the order before resending it:
[Find the order after a timeout](/sdk/python/orders/#find-the-order-after-a-timeout).

### SahiConnectionException

The connection to the host could not be opened: DNS, connection refused, a
TLS error, a connect timeout. Nothing was sent, so the request is safe to
retry once the network is fixed.

### SahiFeedException

Base class for stream errors; carries `close_code`.

### SahiFeedConnectionException

A connection-level refusal: close code `4429` when the account or token
already has five connections open, which clears once another closes; or
`close_code` `None` for a send attempted while the stream is not connected.
A heartbeat drop, a slow-consumer drop or a network drop is not this
exception: those reconnect on their own.

### SahiFeedAuthExpiredException

The token was rejected: close code `4401`, or `401` at the handshake. Mint a
fresh token, build a new feed and subscribe again.

### SahiFeedForbiddenException

The token lacks the stream's scope: `403` at the handshake, or close `4403`.

### SahiFeedNotSubscribedException

A cached value was requested for an instrument that is not subscribed.
