nsip 0.7.2

NSIP Search API client for nsipsearch.nsip.org/api
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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
---
diataxis_type: reference
---
# Error Handling Reference

Complete reference for error handling in the `nsip` crate.

> **Dual-consumer output:** for how errors are *rendered* to humans and LLM
> agents (the RFC 9457 `application/problem+json` envelope, `--format` / TTY
> selection, exit codes, and the per-type catalog), see
> [ERROR-ENVELOPE.md]ERROR-ENVELOPE.md and the [error catalog]errors/.

---

## Error Type

The crate defines a single error enum with six variants, implemented using
`thiserror` and `miette::Diagnostic`. Every variant maps to an RFC 9457 Problem
Details envelope via [`Error::to_problem_details`](ERROR-ENVELOPE.md). The
fallible-API variants carry an optional `#[source]` to preserve the originating
`reqwest` / `serde_json` cause chain, and the transient variants also carry an
optional `retry_after`:

```rust
use miette::Diagnostic;
use thiserror::Error;

/// Boxed, thread-safe source error used to preserve the cause chain.
type BoxSource = Box<dyn std::error::Error + Send + Sync + 'static>;

#[derive(Error, Debug, Diagnostic)]
#[non_exhaustive]
pub enum Error {
    #[error("validation error: {message}")]
    Validation {
        kind: ValidationKind,
        message: String,
    },

    #[error("API error (HTTP {status}): {message}")]
    Api {
        status: u16,
        message: String,
        retry_after: Option<RetryAfter>,
        #[source]
        source: Option<BoxSource>,
    },

    #[error("not found: {0}")]
    NotFound(String),

    #[error("request timed out: {message}")]
    Timeout {
        message: String,
        retry_after: Option<RetryAfter>,
        #[source]
        source: Option<BoxSource>,
    },

    #[error("connection error: {message}")]
    Connection {
        message: String,
        retry_after: Option<RetryAfter>,
        #[source]
        source: Option<BoxSource>,
    },

    #[error("parse error: {message}")]
    Parse {
        message: String,
        #[source]
        source: Option<BoxSource>,
    },
}
```

All variants implement `std::fmt::Display`, `std::error::Error`, and
`miette::Diagnostic`. The enum is `#[non_exhaustive]`, so downstream `match`
expressions must include a wildcard `_ =>` arm. Construct errors with the
provided constructors (e.g. `Error::empty_lpn_id()`, `Error::api(503, "down")`)
rather than building struct variants directly.

---

## Result Type Alias

The crate provides a convenience alias:

```rust
pub type Result<T> = std::result::Result<T, Error>;
```

Use it in your own functions to propagate `nsip` errors:

```rust
async fn fetch_animal(lpn_id: &str) -> nsip::Result<nsip::AnimalDetails> {
    let client = NsipClient::new();
    client.animal_details(lpn_id).await
}
```

---

## Validation Kinds

`Error::Validation` carries a `kind: ValidationKind` that classifies the
specific input failure. Each kind selects a distinct RFC 9457 problem `type` URI
and a tailored recovery hint (see the [error catalog](errors/)). `ValidationKind`
is `#[non_exhaustive]`.

Each kind has a dedicated constructor on `Error`. `empty_lpn_id()` takes no
argument; every other named constructor takes an `impl Into<String>` message.
The generic `Other` kind has no dedicated constructor — use `Error::validation(msg)`
(or `Error::validation_kind(ValidationKind::Other, msg)`).

| `ValidationKind` | Constructor | Meaning |
|------------------|-------------|---------|
| `EmptyLpnId` | `Error::empty_lpn_id()` | An LPN ID argument was empty or blank |
| `InvalidBreedId` | `Error::invalid_breed_id(msg)` | A breed ID was non-positive or unparseable |
| `PageRange` | `Error::page_range(msg)` | A `page` / `page_size` parameter was out of range |
| `EmptySearch` | `Error::empty_search(msg)` | A search request carried no usable filter |
| `CompareArity` | `Error::compare_arity(msg)` | A comparison was given fewer than 2 or more than 5 animals |
| `MissingArgument` | `Error::missing_argument(msg)` | A required MCP argument was absent |
| `UnknownResource` | `Error::unknown_resource(msg)` | An MCP resource URI matched no known resource or template |
| `InvalidCursor` | `Error::invalid_cursor(msg)` | An MCP pagination cursor could not be decoded or was out of range |
| `UnknownTransport` | `Error::unknown_transport(msg)` | An MCP transport other than `stdio` / `http` was requested |
| `Other` | `Error::validation(msg)` | Any other input validation failure (the generic fallback) |

```rust
use nsip::{Error, ValidationKind};

let err = Error::empty_lpn_id();
let generic = Error::validation("something else was wrong");

// Dispatch on the specific kind.
if let Error::Validation { kind, message } = &err {
    match kind {
        ValidationKind::EmptyLpnId => eprintln!("provide a non-empty LPN ID"),
        ValidationKind::CompareArity => eprintln!("pass between 2 and 5 LPN IDs: {message}"),
        _ => eprintln!("validation error: {message}"),
    }
}
```

---

## Error Variants

### `Error::Validation`

Returned when input parameters fail local validation before a request is sent to the API. Carries a [`ValidationKind`](#validation-kinds) and a human-readable `message`.

**Display format:** `validation error: {message}`

**Triggered by:**

| Method | Condition |
|--------|-----------|
| `trait_ranges(breed_id)` | `breed_id <= 0` |
| `search_animals(page, page_size, ...)` | `page_size == 0` or `page_size > 100` |
| `animal_details(search_string)` | `search_string` is empty or whitespace-only |
| `lineage(lpn_id)` | `lpn_id` is empty or whitespace-only |
| `progeny(lpn_id, page, page_size)` | `lpn_id` is empty, or `page_size == 0` |
| `search_by_lpn(lpn_id)` | `lpn_id` is empty or whitespace-only |
| `NsipClientBuilder::build()` | (not applicable -- see `Error::Connection`) |

**Example:**

```rust
use nsip::{NsipClient, Error};

let client = NsipClient::new();

// page_size of 0 triggers Validation
match client.search_animals(0, 0, None, None, None, None).await {
    Err(Error::Validation { kind, message }) => {
        eprintln!("Invalid input ({kind:?}): {message}");
        // Fix the input -- do not retry
    }
    Ok(results) => { /* process results */ }
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** Fix the input parameters. Never retry on validation errors.

---

### `Error::Api`

Returned when the NSIP API responds with a non-success HTTP status code that is not 404 and not retryable (or retries are exhausted for 5xx codes).

**Display format:** `API error (HTTP {status}): {message}`

**Fields:**
- `status: u16` -- the HTTP status code
- `message: String` -- human-readable description (the response body where available)
- `retry_after: Option<RetryAfter>` -- retry delay parsed from the upstream `Retry-After` header, for transient (429 / 5xx) responses
- `source: Option<BoxSource>` -- originating transport error, preserved for the cause chain

**Common status codes:**

| Status | Meaning |
|--------|---------|
| 400 | Bad request -- malformed search criteria |
| 403 | Forbidden -- access denied |
| 500 | Internal server error (after retries exhausted) |
| 502 | Bad gateway (after retries exhausted) |
| 503 | Service unavailable (after retries exhausted) |
| 504 | Gateway timeout (after retries exhausted) |

**Example:**

```rust
match client.breed_groups().await {
    Err(Error::Api { status, message, .. }) => {
        match status {
            400 => eprintln!("Bad request: {}", message),
            500..=599 => eprintln!("Server error ({}): {}", status, message),
            _ => eprintln!("HTTP {}: {}", status, message),
        }
    }
    Ok(groups) => { /* process groups */ }
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** For 4xx errors, check your request parameters. For 5xx errors, the client has already retried according to its retry policy (see [Retry Behavior](#retry-behavior)). You may wait and retry later.

---

### `Error::NotFound`

Returned when the API responds with HTTP 404 -- the requested resource does not exist.

**Display format:** `not found: {message}`

**Triggered by:**
- `animal_details()` when the animal is not in the database
- `lineage()` when the LPN ID has no lineage data
- `progeny()` when the LPN ID has no progeny data
- Any endpoint that returns HTTP 404

**Example:**

```rust
match client.animal_details("NONEXISTENT-ID").await {
    Err(Error::NotFound(msg)) => {
        eprintln!("Not found: {}", msg);
        // Prompt user for a different ID
    }
    Ok(animal) => println!("Found: {}", animal.lpn_id),
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** Verify the LPN ID or search string is correct. Do not retry with the same identifier.

---

### `Error::Timeout`

Returned when the HTTP request exceeds the configured timeout duration. The default timeout is 30 seconds.

**Display format:** `request timed out: {message}`

**Triggered by:**
- Slow network connections
- Large result sets
- Server overload

**Example:**

```rust
match client.search_animals(0, 100, None, None, None, None).await {
    Err(Error::Timeout { message, .. }) => {
        eprintln!("Timed out: {}", message);
        // Reduce page size or increase timeout
        let client = NsipClient::builder()
            .timeout_secs(120)
            .build()?;
    }
    Ok(results) => { /* process results */ }
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** Increase the timeout with `NsipClient::builder().timeout_secs()`, reduce the page size, or retry after a delay.

---

### `Error::Connection`

Returned when the HTTP client cannot establish a connection to the API server.

**Display format:** `connection error: {message}`

**Triggered by:**
- No internet connectivity
- DNS resolution failure
- Firewall blocking the request
- Invalid base URL configured via `NsipClient::with_base_url()` or `NsipClientBuilder::base_url()`
- Failure to build the `reqwest::Client` in `NsipClientBuilder::build()`

**Example:**

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

match client.breed_groups().await {
    Err(Error::Connection { message, .. }) => {
        eprintln!("Connection failed: {}", message);
        // Check network, then retry
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
    Ok(groups) => { /* process groups */ }
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** Check network connectivity and the configured base URL. Retry with exponential backoff.

---

### `Error::Parse`

Returned when the API response cannot be deserialized into the expected data type.

**Display format:** `parse error: {message}`

**Triggered by:**
- Unexpected JSON structure from the API
- Missing required fields in the response
- Invalid data types in the response
- API format changes

**Example:**

```rust
match client.trait_ranges(640).await {
    Err(Error::Parse { message, .. }) => {
        eprintln!("Parse error: {}", message);
        // Likely an API change -- report as a bug
    }
    Ok(ranges) => { /* process ranges */ }
    Err(e) => eprintln!("Other error: {}", e),
}
```

**Recovery:** Parse errors typically indicate an API-side change. Report it as a bug. Do not retry with the same request.

---

## Retry Behavior

The `NsipClient` automatically retries requests that fail with specific server error codes. Retries happen transparently before any error is returned to the caller.

**Retried status codes:** 500, 502, 503, 504

**Default retry policy:**

| Setting | Default | Builder method |
|---------|---------|----------------|
| Max retries | 3 | `NsipClientBuilder::max_retries()` |
| Backoff factor | 0.5 | Not configurable |
| Backoff formula | `0.5 * 2^attempt` seconds | -- |

**Retry delay schedule (with defaults):**

| Attempt | Delay |
|---------|-------|
| 1 | 0.5 seconds |
| 2 | 1.0 seconds |
| 3 | 2.0 seconds |

If all retries are exhausted, the final error is returned as `Error::Api`.

**Customize retry policy:**

```rust
// More aggressive retries
let client = NsipClient::builder()
    .max_retries(5)
    .build()?;

// No retries (fail fast)
let client = NsipClient::builder()
    .max_retries(0)
    .build()?;
```

---

## Error Display Messages

Each variant produces a distinct display prefix:

| Variant | Display prefix |
|---------|---------------|
| `Validation { kind, message }` | `validation error: {message}` |
| `Api { status, message, .. }` | `API error (HTTP {status}): {message}` |
| `NotFound(msg)` | `not found: {msg}` |
| `Timeout { message, .. }` | `request timed out: {message}` |
| `Connection { message, .. }` | `connection error: {message}` |
| `Parse { message, .. }` | `parse error: {message}` |

---

## Matching All Variants

A comprehensive match on all error variants:

```rust
use nsip::{NsipClient, Error};

let client = NsipClient::new();

match client.animal_details("430735-0032").await {
    Ok(animal) => {
        println!("Retrieved: {}", animal.lpn_id);
    }
    Err(Error::Validation { kind, message }) => {
        // Bad input -- fix and do not retry
        eprintln!("Invalid input ({kind:?}): {message}");
    }
    Err(Error::Api { status, message, .. }) => {
        // Server returned an error HTTP status
        eprintln!("API error (HTTP {}): {}", status, message);
    }
    Err(Error::NotFound(msg)) => {
        // Resource does not exist
        eprintln!("Not found: {}", msg);
    }
    Err(Error::Timeout { message, .. }) => {
        // Request exceeded timeout
        eprintln!("Timed out: {}", message);
    }
    Err(Error::Connection { message, .. }) => {
        // Network-level failure
        eprintln!("Connection error: {}", message);
    }
    Err(Error::Parse { message, .. }) => {
        // Response deserialization failed
        eprintln!("Parse error: {}", message);
    }
    // `Error` is #[non_exhaustive]; a wildcard arm is required.
    Err(e) => eprintln!("Unexpected error: {e}"),
}
```

---

## Wrapping in Application Errors

Use `#[from]` with `thiserror` to convert `nsip::Error` into your application's error type:

```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("NSIP error: {0}")]
    Nsip(#[from] nsip::Error),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

async fn process_animal(lpn_id: &str) -> Result<(), AppError> {
    let client = nsip::NsipClient::new();
    let animal = client.animal_details(lpn_id).await?; // converts via From
    Ok(())
}
```

---

## See Also

- [Configuration Reference]CONFIGURATION.md -- timeout and retry settings
- [Library API Reference]LIBRARY-API.md -- method signatures and validation rules
- [How to Configure Timeout and Retries]../how-to/CONFIGURE-CLIENT.md
- [NSIP Data Model]../explanation/NSIP-DATA-MODEL.md