error-envelope 0.3.0

Structured, consistent error responses for Rust APIs. Framework-agnostic with Axum support.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# API Reference

Complete API documentation for error-envelope.

## Table of Contents

- [Common Constructors]#common-constructors
- [Formatted Constructors]#formatted-constructors
- [Custom Errors]#custom-errors
- [Builder Pattern]#builder-pattern
- [Helper Functions]#helper-functions
- [Framework Integration]#framework-integration
- [Advanced Usage]#advanced-usage
- [Domain Error Mapping (thiserror)]#domain-error-mapping-thiserror

---

## Common Constructors

Pre-built constructors for standard HTTP error scenarios.

```rust
use error_envelope::Error;

// Generic errors
Error::internal("Database connection failed");   // 500
Error::bad_request("Invalid JSON in body");       // 400

// Auth errors
Error::unauthorized("Missing token");             // 401
Error::forbidden("Insufficient permissions");     // 403

// Resource errors
Error::not_found("User not found");                // 404
Error::method_not_allowed("POST not allowed");      // 405
Error::request_timeout("Client timeout");          // 408
Error::conflict("Email already exists");           // 409
Error::gone("Resource permanently deleted");      // 410
Error::payload_too_large("Upload exceeds 10MB");    // 413
Error::unprocessable_entity("Invalid data format"); // 422

// Infrastructure errors
Error::rate_limited("Too many requests");          // 429
Error::unavailable("Service temporarily down");   // 503
Error::timeout("Database query timed out");       // 504

// Downstream errors
Error::downstream("payments", err);               // 502
Error::downstream_timeout("payments", err);        // 504
```

---

## Formatted Constructors

Use the `format!` macro for dynamic error messages.

```rust
use error_envelope::{not_foundf, internalf, unauthorizedf, forbiddenf, 
                      conflictf, timeoutf, unavailablef};

// Dynamic messages with format! macro
let user_id = 123;
let err = not_foundf(format!("user {} not found", user_id));

let db_name = "postgres";
let err = internalf(format!("database {} connection failed", db_name));

let role = "admin";
let err = forbiddenf(format!("requires {} role", role));

// All common constructors have 'f' variants
let err = unauthorizedf(format!("token expired at {}", timestamp));
let err = conflictf(format!("email {} already registered", email));
let err = timeoutf(format!("query took {}ms (limit: 5000ms)", duration));
```

---

## Custom Errors

Low-level constructor for full control.

```rust
use error_envelope::{Error, Code};
use std::time::Duration;

// Low-level constructor
let err = Error::new(
    Code::Internal,
    500,
    "Database connection failed"
);

// Add structured details
let err = err.with_details(serde_json::json!({
    "database": "postgres",
    "host": "db.example.com",
    "connection_pool": "primary"
}));

// Add trace ID for distributed tracing
let err = err.with_trace_id("abc-123-def-456");

// Override default retryable behavior
let err = err.with_retryable(true);

// Override HTTP status code
let err = err.with_status(503);

// Set retry-after duration (for rate limiting)
let err = err.with_retry_after(Duration::from_secs(60));
```

### Helper Constructor: `newf`

For formatted messages without separate `*f` functions:

```rust
use error_envelope::{Error, Code};

let user_id = 123;
let err = Error::newf(
    Code::NotFound, 
    404, 
    format!("user {} not found", user_id)
);

// Equivalent to:
let err = not_foundf(format!("user {} not found", user_id));
```

---

## Builder Pattern

All `with_*` methods consume and return `Self`, enabling fluent chaining.

```rust
use error_envelope::Error;
use std::time::Duration;

// Chain multiple modifiers
let err = Error::rate_limited("too many requests")
    .with_details(serde_json::json!({
        "limit": 100,
        "window": "1m",
        "reset_at": "2025-12-25T12:00:00Z"
    }))
    .with_trace_id("trace-123")
    .with_retry_after(Duration::from_secs(30));

// Example output:
// {
//   "code": "RATE_LIMITED",
//   "message": "too many requests",
//   "details": {
//     "limit": 100,
//     "window": "1m",
//     "reset_at": "2025-12-25T12:00:00Z"
//   },
//   "trace_id": "trace-123",
//   "retryable": true,
//   "retry_after": "30s"
// }
```

### Available Builder Methods

| Method | Purpose | Example |
|--------|---------|---------|
| `with_details(value)` | Add structured context | `.with_details(json!({"user_id": "123"}))` |
| `with_trace_id(id)` | Add trace ID for debugging | `.with_trace_id(request_id)` |
| `with_retryable(bool)` | Override retry behavior | `.with_retryable(true)` |
| `with_status(u16)` | Override HTTP status | `.with_status(503)` |
| `with_retry_after(Duration)` | Set retry duration | `.with_retry_after(Duration::from_secs(30))` |

### Immutability

The builder pattern is **immutable by default** in Rust:

```rust
let err1 = Error::internal("base error");
let err2 = err1.with_trace_id("abc-123");

// err1 and err2 are different instances
// err1 does NOT have trace_id (it was moved into err2)
```

This is unlike the Go version (`err-envelope`), which had to implement copy-on-modify explicitly.

---

## Helper Functions

### Validation Errors

```rust
use error_envelope::{validation, FieldErrors};
use std::collections::HashMap;

let mut fields = HashMap::new();
fields.insert("email".to_string(), "Invalid email format".to_string());
fields.insert("age".to_string(), "Must be between 18 and 120".to_string());

let err = validation(fields);

// Response:
// {
//   "code": "VALIDATION_FAILED",
//   "message": "Invalid input",
//   "details": {
//     "fields": {
//       "email": "Invalid email format",
//       "age": "Must be between 18 and 120"
//     }
//   },
//   "retryable": false
// }
```

### Error Mapping

```rust
use error_envelope::from;

// Map arbitrary errors to Error
fn process() -> Result<Data, error_envelope::Error> {
    let result = external_call()
        .map_err(|e| from(e))?;  // Automatically detects timeout/cancel patterns
    Ok(result)
}

// The from() helper inspects error messages and maps to appropriate codes:
// - "timeout" or "timed out" → Timeout (504, retryable)
// - "cancel" → Canceled (499, not retryable)
// - Everything else → Internal (500, not retryable)
```

### Error Code Checking

```rust
use error_envelope::{is, Code};

let err = Error::not_found("User not found");

if is(&err, Code::NotFound) {
    // Handle not found specifically
}
```

---

## Framework Integration

### Axum (axum-support feature)

```rust
use axum::Json;
use error_envelope::Error;

async fn handler() -> Result<Json<User>, Error> {
    let user = find_user("123").await?;
    Ok(Json(user))
}

// Error automatically converts to HTTP response with:
// - Correct status code
// - JSON body with error envelope
// - X-Request-ID header (if trace_id set)
// - Retry-After header (if retry_after set)
```

### anyhow Integration (anyhow-support feature)

```rust
use error_envelope::Error;
use anyhow::Result;

async fn handler() -> Result<Json<Data>, Error> {
    // anyhow::Error converts automatically via From trait
    let config = load_config().await?;
    let data = fetch_data(&config).await?;
    Ok(Json(data))
}

// Any anyhow::Error becomes error_envelope::Error (Internal/500)
// with the error message preserved
```

---

## Advanced Usage

### Wrapping Errors with Context

```rust
use error_envelope::Error;

// Wrap an underlying error with context
let cause = std::io::Error::new(std::io::ErrorKind::NotFound, "config.toml");
let err = Error::wrap(
    Code::Internal,
    500,
    "Failed to load configuration",
    cause
);

// The cause message is stored internally and included in Display output
println!("{}", err);
// Output: Internal: Failed to load configuration (config.toml)

// Access cause via method
if let Some(cause_msg) = err.cause() {
    println!("Root cause: {}", cause_msg);
}
```

### Rate Limiting with Retry-After

```rust
use error_envelope::Error;
use std::time::Duration;

fn check_rate_limit(user_id: &str) -> Result<(), Error> {
    if exceeded_limit(user_id) {
        return Err(
            Error::rate_limited("Too many requests")
                .with_retry_after(Duration::from_secs(30))
                .with_details(serde_json::json!({
                    "limit": 100,
                    "window": "1m"
                }))
        );
    }
    Ok(())
}

// Response includes Retry-After header (with axum-support)
// and retry_after field in JSON body ("30s")
```

### Distributed Tracing

```rust
use axum::extract::Request;
use error_envelope::Error;

async fn handler(req: Request) -> Result<Json<Data>, Error> {
    // Extract trace ID from incoming request
    let trace_id = req.headers()
        .get("X-Request-ID")
        .and_then(|h| h.to_str().ok())
        .map(String::from)
        .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

    // Use trace ID in all errors
    let data = fetch_data().await
        .map_err(|e| Error::from(e).with_trace_id(&trace_id))?;
    
    Ok(Json(data))
}

// All errors in the call chain will include the same trace_id
// for correlation across services
```

---

## Domain Error Mapping (thiserror)

When using `thiserror` for domain errors, implement `From<DomainError> for Error` to map domain semantics to HTTP responses.

This gives you:
- Explicit HTTP status codes and error codes
- Zero boilerplate in handlers via `?`
- No accidental 500s for domain failures

### Pattern

```rust
use error_envelope::{Code, Error};
use thiserror::Error as ThisError;

#[derive(ThisError, Debug)]
pub enum DomainError {
    #[error("user not found")]
    NotFound,

    #[error("email already exists")]
    EmailConflict,

    #[error("database error")]
    Database(#[from] anyhow::Error),
}

impl From<DomainError> for Error {
    fn from(e: DomainError) -> Self {
        match e {
            DomainError::NotFound =>
                Error::new(Code::NotFound, 404, "User not found"),

            DomainError::EmailConflict =>
                Error::new(Code::Conflict, 409, "Email already exists"),

            // Preserve cause message for debugging
            DomainError::Database(cause) =>
                Error::wrap(Code::Internal, 500, "Database failure", cause),
        }
    }
}
```

### Usage in Handlers

```rust
use axum::Json;
use error_envelope::Error;

#[derive(Debug)]
struct User;

async fn handler() -> Result<Json<User>, Error> {
    // DomainError -> Error automatically via From
    let user = get_user().await?;
    Ok(Json(user))
}

async fn get_user() -> Result<User, DomainError> {
    Err(DomainError::NotFound)
}
```

### Notes

- `Error::wrap(...)` stores the cause's string representation for debugging
- This is **not** an error chain — `Error::source()` returns `None`
- Use this pattern for **domain errors** where you know the HTTP semantics
- Use anyhow integration for **unknown/unexpected errors** at boundaries

---

For complete error code reference, see [ERROR_CODES.md](ERROR_CODES.md).