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

# Rust SDK - Error Handling

> NexusError enum variants, match patterns, and failure scenarios for the Rust SDK.

The SDK distinguishes between two classes of failures:

1. **Initial sync failures** - surfaced as `Err(NexusError::...)` from `NexusClient::create`. Refuse to start the application; an empty cache is unsafe to serve.
2. **Background sync / SSE failures** - never returned to the caller. The existing cache continues to be served.

## `NexusError` variants

`NexusError` is a Rust enum. Always use `match` or `if let` on the variants - never match on the `Display` string:

```rust theme={null}
pub enum NexusError {
    Unauthorized,
    NotFound,
    PublicKeyRestricted,
    ServiceKindMismatch,
    SecretNotFound(String),
    SyncFailed(String),
    Billing,
    RateLimited,
    Quarantined { reason: String, expires_at: String },
    WifNotConfigured,
    WifTokenExchangeFailed(String),
    SessionExpired,
    AbAddonNotAvailable,
    Http(Box<ureq::Error>),
    Json(serde_json::Error),
    Io(std::io::Error),
}
```

`NexusError` implements `std::error::Error` and `Display`, so it integrates naturally with `?`, `anyhow`, `thiserror`, and other error-handling crates.

## When each variant is returned

| Variant                              | Returned by                                         | When                                                                                         |
| ------------------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `Unauthorized`                       | `create`, `sync`, token-exchange                    | HTTP 401 - invalid API key, slug/key mismatch, or revoked key                                |
| `NotFound`                           | `create`, `sync`                                    | HTTP 404 - unknown subdomain or missing endpoint                                             |
| `PublicKeyRestricted`                | `get_secret`, `get_secret_file_path`, write methods | Client uses a public key - returned synchronously, before any network call                   |
| `ServiceKindMismatch`                | `get_secret`, `get_secret_file_path`                | Service `kind` is `"frontend"` - frontend services must not hold secrets                     |
| `SecretNotFound(key)`                | `get_secret`, `get_secret_file_path`                | The requested key is not in the cache (SK key confirmed)                                     |
| `SyncFailed(msg)`                    | `create`, `sync`                                    | Network error or unexpected response during sync                                             |
| `Billing`                            | `sync`, `create`                                    | HTTP 402 - tenant has overdue invoices; background sync halted, cache served                 |
| `RateLimited`                        | `sync`, `run_stream` internal                       | HTTP 429 without quarantine body                                                             |
| `Quarantined { reason, expires_at }` | `sync`                                              | HTTP 429 with quarantine body - sync suspended until `expires_at`                            |
| `WifNotConfigured`                   | `create`, `sync` (WIF on)                           | `WifConfig.enabled = true` but no usable provider matched and no `token_source` was supplied |
| `WifTokenExchangeFailed(msg)`        | `create`, `sync` (WIF on)                           | `POST /v1/auth/token-exchange` returned non-2xx or body could not be decoded                 |
| `SessionExpired`                     | `sync`, SSE thread (WIF on)                         | Session JWT expired and refresh failed; no static `api_key` fallback                         |
| `AbAddonNotAvailable`                | `evaluate_ab`                                       | HTTP 403 - project does not have the AB Testing add-on                                       |
| `Http(e)`                            | any network method                                  | Underlying `ureq` transport error                                                            |
| `Json(e)`                            | any network method                                  | Response body could not be deserialised                                                      |
| `Io(e)`                              | `get_secret_file_path`, `sync`                      | File system error writing a file-type secret temp file                                       |

## What is NOT returned as an error

* **Background syncs** - silently retried on the next TTL tick. Cache served.
* **SSE transport errors** - exponential backoff, 3-strike fallback to polling. The background thread exits silently.
* **`304 Not Modified`** - treated as success; TTL reset.
* **Missing config / flag** - `get_config` returns `None`; `get_flag` returns `default_value`.

<Info>
  The asymmetry between secrets (return `Err`) and configs/flags (return `None` / default) is deliberate: a missing secret usually means "you're misconfigured, fail fast", while a missing flag usually means "use the safe default and continue".
</Info>

## Match patterns

### Initial client creation

```rust theme={null}
let client = match NexusClient::create(config) {
    Ok(c) => c,
    Err(NexusError::Unauthorized) => {
        eprintln!("nexus: bad API key or slug mismatch");
        std::process::exit(1);
    }
    Err(NexusError::SyncFailed(msg)) => {
        eprintln!("nexus: initial sync failed: {}", msg);
        std::process::exit(1);
    }
    Err(e) => return Err(e),
};
```

### Reading a secret

```rust theme={null}
match client.get_secret("DB_PASSWORD") {
    Ok(val) => use_secret(val),
    Err(NexusError::PublicKeyRestricted) => {
        eprintln!("public key cannot read secrets - use a secret key");
    }
    Err(NexusError::ServiceKindMismatch) => {
        eprintln!("frontend service has no secrets");
    }
    Err(NexusError::SecretNotFound(key)) => {
        eprintln!("secret '{}' not found in cache", key);
    }
    Err(e) => return Err(e),
}
```

### Quarantine handling

```rust theme={null}
match client.sync() {
    Ok(()) => {}
    Err(NexusError::Quarantined { reason, expires_at }) => {
        eprintln!("nexus: quarantined until {} ({})", expires_at, reason);
        // SDK suspends sync automatically; cache is still served
    }
    Err(NexusError::Billing) => {
        eprintln!("nexus: billing overdue - cache may be stale");
    }
    Err(e) => return Err(e),
}
```

### `PublicKeyRestricted` fires before the network call

`PublicKeyRestricted` is returned synchronously inside `get_secret` - no HTTP request is made. This means the check is instant and cannot fail due to network issues:

```rust theme={null}
// The SDK checks key_type before touching the network:
if let Err(NexusError::PublicKeyRestricted) = client.get_secret("anything") {
    // No network call was made. Safe to use public keys without secrets.
}
```

## 402 Payment Required - `Billing`

When the backend returns HTTP 402 (the tenant has overdue invoices), the SDK:

1. Marks `billing_overdue()` as `true`.
2. Halts the background refresh loop - reads continue to serve the last cached snapshot.
3. Returns `NexusError::Billing` from `sync`.

The flag clears automatically when a subsequent sync succeeds.

```rust theme={null}
if client.billing_overdue() {
    eprintln!("nexus: billing overdue - config refresh suspended; cache may be stale");
}
```

## WIF-specific errors

When `wif.enabled` is `true`:

* `WifNotConfigured` - no usable provider found in the environment and no `token_source` was provided. Either the Kubernetes SA token file is absent, `AWS_ROLE_ARN` is not set, or GCP/Azure metadata endpoints are unreachable. Provide an explicit `token_source` to bypass auto-detection.
* `WifTokenExchangeFailed(msg)` - the backend rejected the OIDC token, or the response body could not be decoded. Most often caused by an audience mismatch or clock skew. Inspect `msg` for the HTTP status code detail.
* `SessionExpired` - fires only when the session JWT expires and the refresh attempt fails, AND no static `api_key` fallback is configured. Provide both `wif` and `api_key` for automatic fallback in degraded conditions.

## Using with `anyhow` or `thiserror`

`NexusError` implements `std::error::Error`, so it composes with popular error-handling crates:

```rust theme={null}
use anyhow::Context;

let client = NexusClient::create(config)
    .context("failed to initialise Nexus client")?;

let secret = client.get_secret("stripe.key")
    .context("missing stripe.key secret")?;
```
