inferadb 0.1.5

Official Rust SDK for InferaDB
Documentation
# Error Handling

The InferaDB SDK provides typed errors that enable precise handling of failure scenarios.

## Error Types

```rust
use inferadb::{Error, ErrorKind};

match vault.check("user:alice", "view", "doc:1").await {
    Ok(allowed) => { /* handle result */ }
    Err(e) => {
        match e.kind() {
            ErrorKind::Unauthorized => { /* credentials invalid */ }
            ErrorKind::Forbidden => { /* insufficient permissions */ }
            ErrorKind::NotFound => { /* resource/vault not found */ }
            ErrorKind::RateLimited => { /* back off and retry */ }
            ErrorKind::SchemaViolation => { /* invalid relation/permission */ }
            ErrorKind::Unavailable => { /* service temporarily down */ }
            ErrorKind::Timeout => { /* request timed out */ }
            _ => { /* other error */ }
        }
    }
}
```

## check() vs require()

The SDK provides two patterns for authorization checks:

| Method      | Returns                    | Use Case                              |
| ----------- | -------------------------- | ------------------------------------- |
| `check()`   | `Result<bool, Error>`      | When you need the boolean value       |
| `require()` | `Result<(), AccessDenied>` | Guard clauses, early-return on denial |

```rust
// check() - returns bool, denial is Ok(false)
let allowed = vault.check("user:alice", "view", "doc:1").await?;
if !allowed {
    return Err(AppError::Forbidden);
}

// require() - denial is Err(AccessDenied), integrates with ?
vault.check("user:alice", "view", "doc:1")
    .require()
    .await?;  // Returns early on denial
```

**Key invariant**: `check()` returns `Ok(false)` for denied access. Only `require()` converts denial to an error.

## AccessDenied Error

The `AccessDenied` error integrates with web frameworks:

```rust
use inferadb::AccessDenied;

// Axum
impl IntoResponse for AccessDenied {
    fn into_response(self) -> Response {
        StatusCode::FORBIDDEN.into_response()
    }
}

// Actix-web
impl ResponseError for AccessDenied {
    fn status_code(&self) -> StatusCode {
        StatusCode::FORBIDDEN
    }
}
```

## Retriable Errors

Check if an error is safe to retry:

```rust
match vault.check(subject, permission, resource).await {
    Ok(allowed) => Ok(allowed),
    Err(e) if e.is_retriable() => {
        // Safe to retry: Unavailable, Timeout, RateLimited
        let delay = e.retry_after().unwrap_or(Duration::from_millis(100));
        tokio::time::sleep(delay).await;
        // Retry...
    }
    Err(e) => Err(e),  // Not retriable
}
```

**Retriable errors**:

| ErrorKind         | Retry? | Notes                         |
| ----------------- | ------ | ----------------------------- |
| `Unavailable`     | Yes    | Service temporarily down      |
| `Timeout`         | Yes    | Request timed out             |
| `RateLimited`     | Yes    | Use `retry_after()` for delay |
| `Unauthorized`    | No     | Fix credentials               |
| `Forbidden`       | No     | Fix permissions               |
| `NotFound`        | No     | Resource doesn't exist        |
| `SchemaViolation` | No     | Fix schema/query              |
| `InvalidArgument` | No     | Fix input                     |

## Request IDs

All errors include request IDs for debugging:

```rust
match vault.check(subject, permission, resource).await {
    Err(e) => {
        if let Some(request_id) = e.request_id() {
            tracing::error!(
                request_id = %request_id,
                error = %e,
                "Authorization check failed"
            );
        }
    }
    Ok(_) => {}
}
```

## Error Context

Errors include context for debugging:

```rust
let e: Error = /* ... */;

// Error kind for matching
e.kind();  // ErrorKind::RateLimited

// Request ID for support
e.request_id();  // Some("req_abc123...")

// Retry guidance for rate limits
e.retry_after();  // Some(Duration::from_secs(5))

// Full error chain
eprintln!("{:#}", e);
```

## Retry Configuration

Configure retry behavior per operation category:

```rust
use inferadb::{RetryConfig, OperationRetry, RetryBudget};

let client = Client::builder()
    .url("https://api.inferadb.com")
    .credentials(creds)
    .retry(RetryConfig::default()
        .max_retries(3)
        .initial_backoff(Duration::from_millis(100))
        .max_backoff(Duration::from_secs(10))
        // Retry budget prevents retry storms
        .retry_budget(RetryBudget::default()
            .retry_ratio(0.1)  // Max 10% retries
            .min_retries_per_second(10))
        // Per-category settings
        .reads(OperationRetry::enabled())
        .idempotent_writes(OperationRetry::enabled())
        .non_idempotent_writes(OperationRetry::connection_only()))
    .build()
    .await?;
```

### Operation Categories

| Category                | Default Behavior       | Notes                             |
| ----------------------- | ---------------------- | --------------------------------- |
| `reads`                 | Retry all errors       | Checks, lookups - always safe     |
| `idempotent_writes`     | Retry all errors       | Writes with request ID            |
| `non_idempotent_writes` | Connection errors only | Safe: request didn't reach server |

### Retry Budget

Prevents cascading failures under load:

```rust
RetryBudget::default()
    .ttl(Duration::from_secs(10))     // Tracking window
    .min_retries_per_second(10)       // Always allow 10/sec
    .retry_ratio(0.1)                 // Max 10% of requests
```

## Request ID for Idempotent Writes

Ensure safe retries for mutations:

```rust
use uuid::Uuid;

// Generate ID once, reuse for retries
let request_id = Uuid::new_v4();

vault.relationships()
    .write(Relationship::new("doc:1", "viewer", "user:alice"))
    .request_id(request_id)
    .await?;

// Safe to retry with same ID - server deduplicates
vault.relationships()
    .write(Relationship::new("doc:1", "viewer", "user:alice"))
    .request_id(request_id)  // Same ID
    .await?;
```

### Auto-Generated Request IDs

```rust
let client = Client::builder()
    .auto_request_id(true)  // Generate UUID for each mutation
    .build()
    .await?;
```

## Graceful Degradation

Configure how the SDK handles failures when the service is unavailable.

### Failure Modes

```rust
use inferadb::FailureMode;

// Per-request override
let allowed = vault.check("user:alice", "view", "doc:1")
    .on_error(FailureMode::FailClosed)  // Deny on error (default)
    .await?;

// Fail-open for non-critical paths (logs WARN)
let allowed = vault.check("user:alice", "view", "doc:public")
    .on_error(FailureMode::FailOpen)
    .await
    .unwrap_or(true);
```

| Mode         | Behavior       | Use Case                                  |
| ------------ | -------------- | ----------------------------------------- |
| `FailClosed` | Deny on error  | Security-critical paths (default)         |
| `FailOpen`   | Allow on error | Non-critical paths, availability priority |
| `Propagate`  | Return error   | Custom fallback logic in application      |

### Degradation Configuration

Configure global fallback strategies for production resilience:

```rust
use inferadb::{DegradationConfig, FailureMode, CheckFallbackStrategy, WriteFallbackStrategy};

let client = Client::builder()
    .url("https://api.inferadb.com")
    .credentials(creds)
    .degradation(DegradationConfig::new()
        // Default: deny on check failure
        .on_check_failure(FailureMode::FailClosed)

        // Use cached decisions when service unavailable
        .on_check_unavailable(CheckFallbackStrategy::UseCache {
            max_age: Duration::from_secs(300),
        })

        // Queue writes for retry when service unavailable
        .on_write_failure(WriteFallbackStrategy::Queue {
            max_queue_size: 1000,
            flush_interval: Duration::from_secs(5),
        })

        // Alert on degradation
        .on_degradation_start(|reason| {
            tracing::warn!("Entering degraded mode: {}", reason);
        })
        .on_degradation_end(|| {
            tracing::info!("Service recovered");
        }))
    .build()
    .await?;
```

### Fallback Strategies

**Check fallbacks** (when authorization service unavailable):

| Strategy               | Behavior                              |
| ---------------------- | ------------------------------------- |
| `Error`                | Return error immediately (default)    |
| `UseCache { max_age }` | Use cached decision if fresh enough   |
| `Default(bool)`        | Return fixed value (use with caution) |
| `Custom(fn)`           | Call custom fallback function         |

**Write fallbacks** (when write operations fail):

| Strategy                                   | Behavior                           |
| ------------------------------------------ | ---------------------------------- |
| `Error`                                    | Return error immediately (default) |
| `Queue { max_queue_size, flush_interval }` | Queue for background retry         |

## Best Practices

1. **Use `require()` for guards** - Cleaner code, integrates with `?`
2. **Log request IDs** - Essential for debugging production issues
3. **Handle rate limits** - Use `retry_after()` for backoff
4. **Fail closed by default** - Only use fail-open for non-critical paths
5. **Categorize errors** - Distinguish user errors from system errors
6. **Use retry budgets** - Prevent retry storms in production
7. **Use request IDs** - Enable safe retries for writes
8. **Configure degradation** - Plan for service unavailability in production