# Errors

Every error code Seal uses, in the SDKs and inside a session: what it means and what to do about it.

Source: https://seal.zone/docs/errors

SDK calls raise a Seal error. Requests inside a session get refused in the protocol's own terms: an HTTP status, a Postgres error or a Redis reply. Both use the same thirteen codes. The caller gets only the code and a fixed message; the full reason goes to your [telemetry](#finding-the-reason-in-telemetry).

## Every error

Retrying won't help with a "No" until something changes. Only `Unavailable` and `SessionExpired` are retryable.

| Error | Meaning | Retry | What to do |
|---|---|---|---|
| `Expired` | The blob is past its `expires` time, or its tenant key version was retired. | No | Seal again and replace the blob. In Python, Node and Rust, `reload_with` (`reloadWith` in Node) picks up the new blob without a restart. See [Rotating and revoking](https://seal.zone/docs/revoking). |
| `NotAllowed` | No grant for the blob's apps, outside the network rule, grant removed mid-session, or no identity found. | No | Check grants with `seal status` and the reason in telemetry. See [Apps and identities](https://seal.zone/docs/apps). |
| `TierTooLow` | The session is below the minimum tier the code or blob asked for. Higher tiers aren't available yet. | No | You only see this if you set a minimum tier. |
| `IntegrityFailed` | The executable hash missed the artifact check, or a signal suspended the identity. | No | Find the signal in telemetry. Wait out a timed suspension, or clear it with `seal settings integrity clear`. |
| `Unavailable` | Something couldn't be reached: a hosted replica starting from idle, a replica with no fresh state ([fails closed](https://seal.zone/docs/limits#freshness-and-failing-closed)), or the upstream. The SDK already tried every replica for up to two minutes. | Yes | Retry with backoff. If it lasts, check `SEAL_ENDPOINT` and telemetry. |
| `PolicyDenied` | A request is outside the blob's policy: path, method, `--deny`, method override, `CONNECT` or WebSocket, compressed response, Redis admin command. Usually an HTTP 403 inside the session. | No | Fix the request. [Limits](https://seal.zone/docs/limits#protocols) lists every rule. |
| `BudgetExceeded` | A per-source rate limit. Plan budgets aren't enforced yet. | No | Slow down. A new session per request is the usual cause; open one and reuse it. |
| `SessionExpired` | The session's token ran out. | Yes | Nothing. The SDK restarts the session itself. |
| `SessionEnded` | Your `close()` or the integrity policy ended the session early. An HTTP connection gets a 401 and closes. | No | Start a new session. |
| `VerificationFailed` | The enclave's hardware report, release, guardian certificate or TLS key didn't check out. It can mean impersonation, so the SDK never retries quietly. | No | Check `SEAL_ENDPOINT` and any TLS-inspecting proxy. Otherwise, stop and look. See [Enclaves and attestation](https://seal.zone/docs/attestation). |
| `BadRequest` | The call is malformed: a bad `to`, app name, header or pin, a secret that breaks the [secret rules](https://seal.zone/docs/limits#secrets), or a damaged blob. | No | Fix the calling code. |
| `Internal` | Seal itself failed. Details go only to telemetry. | Once | If it repeats, send the time and your tenant id to harvey@seal.zone. |
| `ResealRequired` | The blob predates the platform this replica runs on, so it has no key for it. | No | Seal again. The new blob covers every enabled platform. |

## Catching errors

Every SDK error carries a code, a retryable flag and a message.
```python
import os, seal

try:
    with seal.session(os.environ["OPENAI_API_BLOB"]) as s:
        ...
except seal.Expired:
    # Seal the secret again and deploy the new blob
    ...
except seal.SealError as e:
    print(e.code, e.retryable, e.message)
    raise
```

```typescript
import { Client, Expired, SealError } from "@sealzone/sdk";

const seal = await Client.fromEnv();
try {
  const s = await seal.session(process.env.OPENAI_API_BLOB, { kind: "http" });
} catch (e) {
  if (e instanceof Expired) {
    // Seal the secret again and deploy the new blob
  } else if (e instanceof SealError) {
    console.error(e.code, e.retryable, e.message);
  }
  throw e;
}
```

```go
s, err := client.Session(ctx, blob, seal.WithKind(seal.KindHTTP))
if errors.Is(err, seal.ErrExpired) {
	// Seal the secret again and deploy the new blob
}
var se *seal.Error
if errors.As(err, &se) {
	log.Println(se.Code, se.Retryable(), se.Message)
}
```

```rust
match seal.session(&blob).http().await {
    Ok(s) => { /* use s.url(), s.key() */ }
    Err(e) if e.code() == seal::ErrorCode::Expired => {
        // Seal the secret again and deploy the new blob
    }
    Err(e) => eprintln!("{:?} retryable={}: {e}", e.code(), e.code().retryable()),
}
```
| Language | Shape |
|---|---|
| Python | One subclass of `seal.SealError` per code (`seal.Expired`, `seal.NotAllowed`, …) with `.code`, `.retryable`, `.message`. `seal.BadRequest` is also a `ValueError`. |
| Node | Classes from `@sealzone/sdk` extending `SealError`, with `.code` (the wire name, such as `"not_allowed"`), `.retryable`, `.message`. |
| Go | `*seal.Error` with `Code`, `Message`, `Retryable()`. Match with `errors.Is` against `seal.ErrExpired` through `seal.ErrResealRequired`. |
| Rust | `seal::ClientError`, with `e.code()` returning a `seal::ErrorCode` and `ErrorCode::retryable()`. |

## Refusals inside a session

Your app talks to its usual client library, so refusals arrive in that protocol's format. Anything the upstream sends, its own errors included, passes through unchanged apart from key masking.

### HTTP

The enclave answers with a status and a JSON body. The `seal_error` field marks the refusal as Seal's, not the upstream's.

```json
{
  "seal_error": "policy_denied",
  "message": "the request is outside the blob's policy"
}
```

| Status | Codes |
|---|---|
| 400 | `bad_request` |
| 401 | `session_expired`, `session_ended` |
| 403 | `not_allowed`, `tier_too_low`, `integrity_failed`, `policy_denied` |
| 409 | `reseal_required` |
| 410 | `expired` |
| 429 | `budget_exceeded` |
| 495 | `verification_failed` |
| 500 | `internal` |
| 503 | `unavailable` |

The message is one of a fixed set:

| Message | Code | Connection |
|---|---|---|
| the request is outside the blob's policy | `policy_denied` | Stays open |
| the request carries no Seal credential | `not_allowed` | Closes |
| the Seal credential is not this connection's | `not_allowed` | Closes |
| the Seal credential was refused | varies | Closes |
| the blob is not for an HTTP destination | `policy_denied` | Closes |
| the request body is too large to check | `policy_denied` | Closes |
| the upstream sent an encoded response, which cannot be scrubbed | `policy_denied` | Stays open |
| the request was refused by the tenant's integrity policy | varies | Stays open |
| the session was ended by the tenant's integrity policy | `session_ended` | Closes |
| the session has ended | `session_ended` | Closes |
| the upstream could not be reached | `unavailable` | Stays open |
| the upstream request failed | `unavailable` | Stays open |

"No Seal credential" almost always means the library didn't send the stand-in key. Pass `s.key` where it expects its API key.

### Postgres

The error text starts with `seal:` and ends with the code, such as `seal: … (policy_denied)`.

| SQLSTATE | Codes |
|---|---|
| `42501` | `policy_denied`, `not_allowed`, `integrity_failed`, `tier_too_low` |
| `28000` | `expired`, `session_expired`, `session_ended`, `reseal_required`, `verification_failed` |
| `57P01` | The session ended while the connection was open |
| `08006` | `unavailable` |
| `53400` | `budget_exceeded` |
| `08P01` | `bad_request` |
| `XX000` | `internal` |

### Redis

A command the blob doesn't allow gets `-NOPERM seal policy_denied: …`, and the connection stays open. When the session ends, the client gets `-ERR seal <code>: …` and the connection closes.

## Finding the reason in telemetry

A refused session shows up as a `Refused` entry. A refused request shows up as a denied `Request` entry with the code. Filter the console's Telemetry page by app, identity, blob or event, or run `seal telemetry --since 1h`. Entries never hold the secret, bodies or upstream text.

| Reason | What happened | Caller got |
|---|---|---|
| `no_grant` | The identity matches no grant for the blob's apps. The entry shows the identity seen. | `NotAllowed` |
| `unknown_app_key` | An app key that no grant names. | `NotAllowed` |
| `outside_networks` | Connected from outside the app's networks. The entry shows the address. | `NotAllowed` |
| `grant_lost` | A live session's grant was removed. | `NotAllowed` |
| `bad_proof`, `token_expired` | The identity proof didn't verify, or its token was too old. | `NotAllowed` |
| `suspended` | A signal suspended the identity. The entry shows the code and until when. | `IntegrityFailed` |
| `artifact_mismatch` | The executable hash isn't on the artifact list. | `IntegrityFailed` |
| `blob_expired` | Past the blob's `expires` time. | `Expired` |
| `key_version_retired` | The blob's tenant key version is retired. | `Expired` |
| `not_sealed_for_platform` | The blob predates this replica's platform. | `ResealRequired` |
| `tier_too_low` | The session's tier was below what was required. | `TierTooLow` |
| `stale_state`, `no_state` | The replica had no fresh tenant state. | `Unavailable` |
| `unavailable` | A service didn't answer: the control plane, the key guardian or an identity issuer. | `Unavailable` |
| `malformed` | The request was malformed. The entry says how. | `BadRequest` |
