> ## 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.

# Go SDK - Error Handling

> Sentinel errors, when each is returned, and failure scenarios for the Go SDK.

The SDK distinguishes between two classes of failures:

1. **Initial sync failures** - surfaced as a non-nil error from `NewClient`. Refuse to start the application; an empty cache is unsafe to serve.
2. **Background sync / SSE failures** - never returned to the caller. They are logged via `Logger.Errorf(...)`, and the existing cache continues to be served.

## Sentinel errors

The SDK exports the following sentinel error variables. Always check with `errors.Is(err, ...)` rather than equality or string matching:

```go theme={null}
import "errors"

var (
    nexus.ErrSyncFailed             // wraps the cause when NewClient's initial sync fails
    nexus.ErrUnauthorized           // HTTP 401
    nexus.ErrNotFound               // HTTP 404
    nexus.ErrPublicKeyRestricted    // GetSecret with public key
    nexus.ErrSecretNotFound         // unknown secret key
    nexus.ErrBilling                // HTTP 402 - overdue invoices
    nexus.ErrServiceKindMismatch    // GetSecret on kind=frontend
    nexus.ErrABAddonNotAvailable    // EvaluateAB without the AB Testing add-on
    nexus.ErrRateLimited            // HTTP 429 on write endpoints
    nexus.ErrWIFNotConfigured       // WIF on but no provider matched
    nexus.ErrWIFTokenExchangeFailed // /token-exchange returned non-2xx
    nexus.ErrSessionExpired         // session JWT expired, refresh failed
)
```

This is **idiomatic Go** - sentinel errors compose cleanly with `errors.Is` and `errors.As`, and they're the way `io.EOF`, `os.ErrNotExist`, `sql.ErrNoRows`, etc. work in the standard library.

## When each is returned

| Sentinel                    | Returned by                                        | When                                                                                                                                                                              |
| --------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ErrSyncFailed`             | `NewClient`                                        | Wraps the underlying cause (often `ErrUnauthorized` or a transport-level `*url.Error`). Use `errors.Unwrap` to inspect.                                                           |
| `ErrUnauthorized`           | `NewClient`, `Sync`, token-exchange                | Server returned HTTP 401 - invalid API key, slug/key mismatch, or revoked key.                                                                                                    |
| `ErrNotFound`               | `NewClient`, `Sync`                                | Server returned HTTP 404 - unknown subdomain or missing endpoint.                                                                                                                 |
| `ErrPublicKeyRestricted`    | `GetSecret`                                        | The client uses a public key. Returned synchronously, before the network call.                                                                                                    |
| `ErrSecretNotFound`         | `GetSecret`                                        | The requested key is not in the cache (and the client uses an secret key).                                                                                                        |
| `ErrBilling`                | `Sync`                                             | Server returned HTTP 402 - tenant has overdue invoices. The SDK halts background sync and serves the last cached snapshot. Cleared automatically when a subsequent sync succeeds. |
| `ErrServiceKindMismatch`    | `GetSecret`                                        | The service's `kind` is `"frontend"`. Frontend services are public by design and must not hold secrets.                                                                           |
| `ErrABAddonNotAvailable`    | `EvaluateAB`                                       | Server returned HTTP 403 - the project does not have the AB Testing add-on enabled.                                                                                               |
| `ErrRateLimited`            | `SetSecret`, `DeleteSecret`, `DeleteSecretVersion` | Server returned HTTP 429 - too many write requests. Back off and retry.                                                                                                           |
| `ErrWIFNotConfigured`       | `NewClient`, `Sync` (when WIF on)                  | `WIFConfig.Enabled=true` but no usable provider matched (auto-detect failed and no `TokenSource` was supplied).                                                                   |
| `ErrWIFTokenExchangeFailed` | `NewClient`, `Sync` (when WIF on)                  | `POST /v1/auth/token-exchange` returned a non-2xx response or the body could not be decoded.                                                                                      |
| `ErrSessionExpired`         | `Sync`, `RunStream` (when WIF on)                  | The session JWT expired and refresh failed; no static `APIKey` fallback available.                                                                                                |

## 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. `RunStream` returns normally.
* **`304 Not Modified`** - treated as success; TTL reset.
* **Missing config / flag** - `GetConfig` returns `(nil, false)`; `GetFlag` returns the `defaultValue`.

<Info>
  The asymmetry between secrets (return error) and configs/flags (return `ok`/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>

## 402 Payment Required - `ErrBilling`

When the backend returns HTTP 402 (the tenant has at least one invoice overdue by more than 14 days), the SDK:

1. Marks the client's billing-overdue flag (`client.BillingOverdue() == true`).
2. Halts the background refresh loop - read methods continue to serve the last cached snapshot.
3. Returns `ErrBilling` from `Sync(...)`.

The flag clears automatically as soon as a subsequent `Sync` succeeds.

```go theme={null}
if client.BillingOverdue() {
    log.Warn("nexus: billing overdue - config refresh suspended; cache may be stale")
}
```

## WIF-specific errors

When `WIFConfig.Enabled` is true:

* `ErrWIFNotConfigured` - the configured (or auto-detected) provider isn't usable in the current environment. Either the K8s service-account token file is missing, the `AWS_WEB_IDENTITY_TOKEN_FILE` env var isn't set, or the GCP/Azure metadata endpoint isn't reachable. Pass an explicit `TokenSource` to bypass auto-detection.
* `ErrWIFTokenExchangeFailed` - the backend rejected the OIDC token, or the response could not be decoded. Inspect the wrapped error for the HTTP status. Most often caused by an audience mismatch or a clock skew between the provider and the Nexus backend.
* `ErrSessionExpired` - only fires when a refresh attempt fails AND no static `APIKey` fallback is configured. Provide both `WIF` and `APIKey` if you need automatic fallback in degraded conditions.

## `ErrServiceKindMismatch` (frontend services)

`GetSecret(...)` on a service whose `kind` is `"frontend"` returns `ErrServiceKindMismatch`. Check `client.Kind()` to know the kind:

```go theme={null}
if client.Kind() == "frontend" {
    // Don't even try to read secrets - frontend services don't hold any.
}
```

## Failure scenarios

### Wrong API key

```go theme={null}
client, err := nexus.NewClient(ctx, cfg)
switch {
case errors.Is(err, nexus.ErrUnauthorized):
    log.Fatal("nexus: bad API key or slug mismatch")
case errors.Is(err, nexus.ErrSyncFailed):
    log.Fatalf("nexus: initial sync failed: %v", err)
case err != nil:
    log.Fatalf("nexus: %v", err)
}
```

`ErrSyncFailed` is the outer error; the cause is wrapped - `errors.Is(err, nexus.ErrUnauthorized)` matches transitively.

### Slug doesn't match key (Double-Gate failure)

In production, the backend validates that the slug embedded in `BaseURL` matches the API key's service. A mismatch returns 401 - same surface as above.

### Network down at startup

If the Nexus backend is unreachable when `NewClient` runs, the function returns an error wrapping `ErrSyncFailed` whose cause is a `*url.Error`. The decision is yours: crash, or wrap with retry/backoff.

```go theme={null}
var client *nexus.Client
for i := 0; i < 5; i++ {
    var err error
    client, err = nexus.NewClient(ctx, cfg)
    if err == nil {
        break
    }
    log.Printf("nexus: retry %d: %v", i, err)
    time.Sleep(time.Duration(1<<i) * time.Second)
}
if client == nil {
    log.Fatal("nexus: cannot connect after 5 retries")
}
```

### Reading a secret with a public key

```go theme={null}
val, err := client.GetSecret("DB_PASSWORD")
if errors.Is(err, nexus.ErrPublicKeyRestricted) {
    log.Fatal("nexus: public keys cannot read secrets - use a secret key")
}
```

This check fires synchronously, before the network call.

### Reading a secret that doesn't exist

```go theme={null}
val, err := client.GetSecret("does.not.exist")
if errors.Is(err, nexus.ErrSecretNotFound) {
    log.Printf("missing secret - using default")
    val = "default-value"
}
```

## SSE-specific behaviour

`RunStream` failures **never** propagate to your code as errors. The SDK logs them at `Errorf` level and returns when:

* The context is cancelled (clean shutdown)
* 3 consecutive transport errors exhaust the backoff (silent fallback)

```
nexus: SSE transport error (attempt 1/3)
nexus: SSE transport error (attempt 2/3)
nexus: SSE 3 consecutive transport errors - falling back to TTL polling
```

If you need to detect "live updates are no longer available", launch `RunStream` with a `done` channel and check whether it returned before `ctx.Done()`:

```go theme={null}
done := make(chan struct{})
go func() { defer close(done); client.RunStream(ctx) }()

select {
case <-ctx.Done():
    // normal shutdown
case <-done:
    // fell back to polling early - log a metric, alert ops, etc.
}
```
