The InferaDB SDK provides typed errors that enable precise handling of failure scenarios.
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 */ }
}
}
}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 |
// 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 denialKey invariant: check() returns Ok(false) for denied access. Only require() converts denial to an error.
The AccessDenied error integrates with web frameworks:
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
}
}Check if an error is safe to retry:
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 |
All errors include request IDs for debugging:
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(_) => {}
}Errors include context for debugging:
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);Configure retry behavior per operation category:
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?;| 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 |
Prevents cascading failures under load:
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 requestsEnsure safe retries for mutations:
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?;let client = Client::builder()
.auto_request_id(true) // Generate UUID for each mutation
.build()
.await?;Configure how the SDK handles failures when the service is unavailable.
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 |
Configure global fallback strategies for production resilience:
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?;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 |
- Use
require()for guards - Cleaner code, integrates with? - Log request IDs - Essential for debugging production issues
- Handle rate limits - Use
retry_after()for backoff - Fail closed by default - Only use fail-open for non-critical paths
- Categorize errors - Distinguish user errors from system errors
- Use retry budgets - Prevent retry storms in production
- Use request IDs - Enable safe retries for writes
- Configure degradation - Plan for service unavailability in production