providers_kit/error.rs
1//! Structured failures and explicitly accessed, potentially sensitive native diagnostics.
2//!
3//! [`KitError`] formats only classification, schema labels, and numeric context. Native
4//! messages, identifiers and bodies are available through [`KitError::native_details`]
5//! for deliberate inspection; they must not be copied into ordinary logs.
6
7use std::fmt;
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12const DETAIL_STRING_LIMIT: usize = 65_536;
13const DETAIL_TOTAL_LIMIT: usize = 1_048_576;
14const DETAIL_RECORD_LIMIT: usize = 256;
15const ERROR_DEPTH_LIMIT: usize = 8;
16const ERROR_NODE_LIMIT: usize = 32;
17
18/// Evidence used to choose a portable classification; it does not imply retry safety.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
21pub enum ClassificationBasis {
22 /// A caller implementation explicitly selected the classification.
23 Explicit,
24 /// The status used for classification, independently of any status in the body.
25 HttpStatus {
26 /// Actual HTTP status used for this classification.
27 status: u16,
28 },
29 /// A recognized native service code.
30 NativeCode,
31 /// A recognized inner native error type, not the envelope or event type.
32 NativeErrorType,
33 /// A recognized canonical status name.
34 CanonicalStatus,
35 /// A recognized numeric RPC status.
36 RpcCode,
37 /// A recognized reason and domain in the first retained reason record.
38 NativeReason,
39 /// A native hosted outcome or an actual nonzero execution exit code.
40 NativeOutcome,
41 /// A remote failure without a more specific supported classification.
42 Unclassified,
43}
44
45/// A Kit-owned input invariant identified without disclosing caller values.
46///
47/// Labels are trusted static schema names and rules, never native diagnostic text.
48/// Caller implementations can attach one with [`KitError::with_validation_violation`].
49///
50/// ```
51/// use providers_kit::error::ValidationViolation;
52/// let violation = ValidationViolation::OutOfRange {
53/// field: "generation.temperature", rule: "finite and non-negative",
54/// };
55/// assert!(format!("{violation:?}").contains("generation.temperature"));
56/// ```
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum ValidationViolation {
59 /// A required field was not supplied.
60 MissingField {
61 /// Trusted field name.
62 field: &'static str,
63 },
64 /// A required value or collection was empty.
65 Empty {
66 /// Trusted field name.
67 field: &'static str,
68 },
69 /// A value did not satisfy its required syntax.
70 InvalidFormat {
71 /// Trusted field name.
72 field: &'static str,
73 /// Trusted syntax rule, without the rejected value.
74 rule: &'static str,
75 },
76 /// A value violated a non-range invariant.
77 InvalidValue {
78 /// Trusted field name.
79 field: &'static str,
80 /// Trusted value constraint.
81 rule: &'static str,
82 },
83 /// A value was outside its permitted range.
84 OutOfRange {
85 /// Trusted field name.
86 field: &'static str,
87 /// Trusted range description.
88 rule: &'static str,
89 },
90 /// A value was repeated where uniqueness is required.
91 Duplicate {
92 /// Trusted field name.
93 field: &'static str,
94 },
95 /// A value or collection exceeded a known limit.
96 LimitExceeded {
97 /// Trusted field name.
98 field: &'static str,
99 /// Maximum permitted count or size.
100 limit: usize,
101 },
102 /// Two fields cannot be used together.
103 Incompatible {
104 /// Trusted field name.
105 field: &'static str,
106 /// Trusted name of the incompatible field.
107 other: &'static str,
108 },
109 /// The selected implementation cannot support the requested input feature.
110 Unsupported {
111 /// Trusted field name.
112 field: &'static str,
113 /// Trusted capability constraint.
114 rule: &'static str,
115 },
116}
117
118/// JSON decoding category, retained independently of a formatted parser message.
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120pub enum JsonErrorCategory {
121 /// Input/output failed during decoding.
122 Io,
123 /// Invalid JSON syntax.
124 Syntax,
125 /// Valid JSON did not match the expected schema.
126 Data,
127 /// Input ended before a JSON value was complete.
128 Eof,
129}
130
131/// Observed JSON value shape, including the absence of a required field.
132#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
133pub enum JsonValueKind {
134 /// The field is absent.
135 Absent,
136 /// JSON null.
137 Null,
138 /// A JSON boolean.
139 Boolean,
140 /// A JSON number.
141 Number,
142 /// A JSON string.
143 String,
144 /// A JSON array.
145 Array,
146 /// A JSON object.
147 Object,
148}
149
150impl JsonValueKind {
151 pub(crate) fn of(value: Option<&serde_json::Value>) -> Self {
152 match value {
153 None => Self::Absent,
154 Some(serde_json::Value::Null) => Self::Null,
155 Some(serde_json::Value::Bool(_)) => Self::Boolean,
156 Some(serde_json::Value::Number(_)) => Self::Number,
157 Some(serde_json::Value::String(_)) => Self::String,
158 Some(serde_json::Value::Array(_)) => Self::Array,
159 Some(serde_json::Value::Object(_)) => Self::Object,
160 }
161 }
162}
163
164/// Exact response observations, accessed deliberately because values may be sensitive.
165///
166/// The corresponding trusted field name remains in [`ResponseViolation`].
167/// Formatting this value never prints observed strings.
168#[derive(Clone, Eq, PartialEq, Serialize)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum ResponseDetails {
171 /// Parser location and category; line and column use the parser's numbering.
172 Json {
173 /// Parser error category.
174 category: JsonErrorCategory,
175 /// One-based line, when available from the parser.
176 line: usize,
177 /// Parser column, which may be zero for an immediate input failure.
178 column: usize,
179 },
180 /// A field was absent or had the wrong JSON shape.
181 FieldType {
182 /// Required shape.
183 expected: JsonValueKind,
184 /// Observed shape.
185 actual: JsonValueKind,
186 },
187 /// Unrecognized native discriminator text.
188 Discriminator {
189 /// Exact observed discriminator, subject to documented bounds.
190 actual: String,
191 },
192 /// Two identifiers did not agree.
193 Identity {
194 /// Expected identifier.
195 expected: String,
196 /// Received identifier.
197 actual: String,
198 },
199 /// An index was outside the available collection.
200 Index {
201 /// Received index.
202 index: usize,
203 /// Number of available entries.
204 available: usize,
205 },
206 /// Observed bytes when a response limit was exceeded.
207 Size {
208 /// Observed byte count; this may be a lower bound after early termination.
209 observed: usize,
210 },
211}
212
213impl ResponseDetails {
214 pub(crate) fn json(error: &serde_json::Error) -> Self {
215 Self::Json {
216 category: match error.classify() {
217 serde_json::error::Category::Io => JsonErrorCategory::Io,
218 serde_json::error::Category::Syntax => JsonErrorCategory::Syntax,
219 serde_json::error::Category::Data => JsonErrorCategory::Data,
220 serde_json::error::Category::Eof => JsonErrorCategory::Eof,
221 },
222 line: error.line(),
223 column: error.column(),
224 }
225 }
226
227 fn bound(&mut self, budget: &mut DetailBudget) {
228 match self {
229 Self::Discriminator { actual } => budget.string(actual),
230 Self::Identity { expected, actual } => {
231 budget.string(expected);
232 budget.string(actual);
233 }
234 Self::Json { .. } | Self::FieldType { .. } | Self::Index { .. } | Self::Size { .. } => {
235 }
236 }
237 }
238}
239
240impl fmt::Debug for ResponseDetails {
241 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::Json {
244 category,
245 line,
246 column,
247 } => formatter
248 .debug_struct("Json")
249 .field("category", category)
250 .field("line", line)
251 .field("column", column)
252 .finish(),
253 Self::FieldType { expected, actual } => formatter
254 .debug_struct("FieldType")
255 .field("expected", expected)
256 .field("actual", actual)
257 .finish(),
258 Self::Index { index, available } => formatter
259 .debug_struct("Index")
260 .field("index", index)
261 .field("available", available)
262 .finish(),
263 Self::Size { observed } => formatter
264 .debug_struct("Size")
265 .field("observed", observed)
266 .finish(),
267 Self::Discriminator { .. } => {
268 formatter.write_str("Discriminator { actual: <redacted> }")
269 }
270 Self::Identity { .. } => {
271 formatter.write_str("Identity { expected: <redacted>, actual: <redacted> }")
272 }
273 }
274 }
275}
276
277/// Portable failure classification. This classification does not promise retry safety.
278#[derive(Clone, Copy, Debug, Eq, PartialEq)]
279pub enum ErrorKind {
280 /// Invalid provider configuration.
281 Configuration,
282 /// Invalid input detected before provider execution.
283 InvalidInput,
284 /// The implementation cannot perform the requested operation.
285 Unsupported,
286 /// The connection or transport failed without a more authoritative rejection.
287 Transport,
288 /// A local operation deadline elapsed.
289 Timeout,
290 /// The service reported that its deadline elapsed.
291 DeadlineExceeded,
292 /// Lifecycle admission is closed.
293 Closed,
294 /// The operation is incompatible with the current local state.
295 InvalidState,
296 /// The response violates a known schema or protocol invariant.
297 InvalidResponse(ResponseViolation),
298 /// The service rejected the request as invalid.
299 InvalidRequest,
300 /// Authentication was rejected.
301 Authentication,
302 /// The authenticated caller lacks permission.
303 PermissionDenied,
304 /// A requested model or resource was not found.
305 NotFound,
306 /// The request conflicts with current service state.
307 Conflict,
308 /// The request exceeds the service's size limit.
309 RequestTooLarge,
310 /// A service precondition is not satisfied.
311 FailedPrecondition,
312 /// Capacity or quota was exhausted; the service did not distinguish which.
313 ResourceExhausted,
314 /// The service explicitly identified a rate limit.
315 RateLimited,
316 /// The service explicitly identified exhausted quota.
317 QuotaExceeded,
318 /// Billing prevents execution.
319 Billing,
320 /// The service is unavailable or overloaded.
321 ServiceUnavailable,
322 /// The service reported an internal error.
323 Internal,
324 /// The service reported cancellation.
325 Cancelled,
326 /// A remote rejection without a recognized portable category.
327 ProviderRejected,
328}
329
330impl ErrorKind {
331 pub(crate) fn from_http_status(status: u16) -> Self {
332 match status {
333 400 | 422 => Self::InvalidRequest,
334 401 => Self::Authentication,
335 402 => Self::Billing,
336 403 => Self::PermissionDenied,
337 404 => Self::NotFound,
338 408 | 504 => Self::DeadlineExceeded,
339 409 => Self::Conflict,
340 412 => Self::FailedPrecondition,
341 413 => Self::RequestTooLarge,
342 429 => Self::ResourceExhausted,
343 500 => Self::Internal,
344 502 | 503 | 529 => Self::ServiceUnavailable,
345 _ => Self::ProviderRejected,
346 }
347 }
348 pub(crate) fn from_canonical_status(status: Option<&str>) -> ErrorKind {
349 match status {
350 Some("INVALID_ARGUMENT" | "OUT_OF_RANGE") => Self::InvalidRequest,
351 Some("UNAUTHENTICATED") => Self::Authentication,
352 Some("PERMISSION_DENIED") => Self::PermissionDenied,
353 Some("NOT_FOUND") => Self::NotFound,
354 Some("ALREADY_EXISTS" | "ABORTED") => Self::Conflict,
355 Some("RESOURCE_EXHAUSTED") => Self::ResourceExhausted,
356 Some("FAILED_PRECONDITION") => Self::FailedPrecondition,
357 Some("DEADLINE_EXCEEDED") => Self::DeadlineExceeded,
358 Some("CANCELLED") => Self::Cancelled,
359 Some("UNAVAILABLE") => Self::ServiceUnavailable,
360 Some("INTERNAL" | "DATA_LOSS" | "UNKNOWN") => Self::Internal,
361 Some("UNIMPLEMENTED") => Self::Unsupported,
362 _ => Self::ProviderRejected,
363 }
364 }
365
366 pub(crate) fn from_rpc_code(code: Option<i64>) -> ErrorKind {
367 match code {
368 Some(1) => Self::Cancelled,
369 Some(2 | 13 | 15) => Self::Internal,
370 Some(3 | 11) => Self::InvalidRequest,
371 Some(4) => Self::DeadlineExceeded,
372 Some(5) => Self::NotFound,
373 Some(6 | 10) => Self::Conflict,
374 Some(7) => Self::PermissionDenied,
375 Some(8) => Self::ResourceExhausted,
376 Some(9) => Self::FailedPrecondition,
377 Some(12) => Self::Unsupported,
378 Some(14) => Self::ServiceUnavailable,
379 Some(16) => Self::Authentication,
380 _ => Self::ProviderRejected,
381 }
382 }
383}
384
385/// A response invariant, assigned at its detection site without interpreting prose.
386///
387/// Field and rule labels are trusted, static schema descriptions. Implementors must
388/// never place credentials, payloads, URLs or native messages in these labels.
389#[derive(Clone, Copy, Debug, Eq, PartialEq)]
390pub enum ResponseViolation {
391 /// JSON could not be decoded into the expected response schema.
392 MalformedJson,
393 /// Text bytes were not valid UTF-8.
394 InvalidUtf8,
395 /// A required response field was absent.
396 MissingField {
397 /// Static schema field name, never the field's value.
398 field: &'static str,
399 },
400 /// A present response field had an invalid value.
401 InvalidField {
402 /// Static schema field name, never the field's value.
403 field: &'static str,
404 },
405 /// A response discriminator was not recognized.
406 UnknownVariant {
407 /// Static discriminator field name.
408 field: &'static str,
409 },
410 /// Response identifiers did not agree.
411 IdentityMismatch {
412 /// Static identity field name.
413 field: &'static str,
414 },
415 /// The response violated an ordering or semantic invariant.
416 ProtocolViolation {
417 /// Trusted, static description of the invariant.
418 rule: &'static str,
419 },
420 /// A response stream ended before its terminal event.
421 UnexpectedEnd,
422 /// Response data exceeded a byte limit.
423 LimitExceeded {
424 /// Maximum permitted bytes.
425 limit: usize,
426 },
427}
428
429impl ResponseViolation {
430 pub(crate) fn from_json_error(error: &serde_json::Error, field: &'static str) -> Self {
431 match error.classify() {
432 serde_json::error::Category::Data => Self::InvalidField { field },
433 serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
434 Self::MalformedJson
435 }
436 serde_json::error::Category::Io => Self::ProtocolViolation {
437 rule: "JSON input read failure",
438 },
439 }
440 }
441}
442
443/// Caller-visible operation associated with a failure.
444#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub enum Operation {
446 /// No enclosing behavioral call is available (for example, a standalone DTO check).
447 Unspecified,
448 /// Provider construction.
449 Configure,
450 /// Standalone input validation.
451 ValidateInput,
452 /// Exact model selection.
453 SelectModel,
454 /// Model discovery.
455 ListModels,
456 /// Model metadata lookup.
457 ModelInfo,
458 /// File upload.
459 UploadFile,
460 /// File listing.
461 ListFiles,
462 /// File metadata lookup.
463 FileInfo,
464 /// File deletion.
465 DeleteFile,
466 /// Bounded file download.
467 DownloadFile,
468 /// Batch status lookup.
469 BatchStatus,
470 /// Batch cancellation.
471 CancelBatch,
472 /// Bounded batch result retrieval and normalization.
473 BatchResults,
474 /// Batch listing.
475 ListBatches,
476 /// Runtime health check.
477 Health,
478 /// Shared provider shutdown.
479 Shutdown,
480 /// Unary generation.
481 Generate,
482 /// Streaming generation, including stream consumption.
483 GenerateStream,
484 /// Embedding generation.
485 Embed,
486 /// Batch submission.
487 SubmitBatch,
488 /// Stateful session opening.
489 OpenSession,
490 /// Sending a session command.
491 SessionSend,
492 /// Receiving a session event.
493 SessionReceive,
494 /// Session closure.
495 SessionClose,
496}
497
498/// Stage at which the failure was observed; it does not assert that execution is safe to retry.
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum ErrorPhase {
501 /// A caller implementation supplied no finer phase.
502 Unspecified,
503 /// Provider configuration validation.
504 Configuration,
505 /// Local request validation.
506 InputValidation,
507 /// Request encoding or construction.
508 RequestBuild,
509 /// Sending a request or opening its transport.
510 RequestSend,
511 /// Reading or validating the response status and headers.
512 ResponseHeaders,
513 /// Reading response bytes.
514 ReadBody,
515 /// Decoding a response or frame.
516 Decode,
517 /// Converting decoded native data to portable values.
518 Normalize,
519 /// A service error or protocol failure during a generation stream.
520 Stream,
521 /// Session connection negotiation.
522 SessionHandshake,
523 /// Sending a session command.
524 SessionSend,
525 /// Receiving a session event.
526 SessionReceive,
527 /// Closing a session connection.
528 SessionClose,
529 /// Lifecycle admission or shutdown.
530 Lifecycle,
531 /// Releasing owned resources after an operation.
532 Cleanup,
533}
534
535/// Safe operation context, including the location of a failed batch result row.
536///
537/// ```
538/// use providers_kit::error::{ErrorContext, ErrorPhase, Operation};
539/// let context = ErrorContext {
540/// operation: Operation::BatchResults, phase: ErrorPhase::Normalize, row: Some(0),
541/// };
542/// assert_eq!(context.row, Some(0));
543/// ```
544#[derive(Clone, Copy, Debug, Eq, PartialEq)]
545pub struct ErrorContext {
546 /// The enclosing public operation.
547 pub operation: Operation,
548 /// The stage at which this error was detected.
549 pub phase: ErrorPhase,
550 /// Zero-based logical batch result row, excluding blank JSONL lines.
551 pub row: Option<usize>,
552}
553
554/// One structured native fact with its meaning preserved across providers.
555///
556/// Unknown native fields remain in the bounded body instead of an untyped public map.
557#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
558#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
559pub enum NativeDetail {
560 /// A reason is meaningful within its reported domain.
561 Reason {
562 /// Native reason domain.
563 domain: String,
564 /// Native reason value.
565 reason: String,
566 },
567 /// A resource explicitly identified by the service, not a caller correlation ID.
568 Resource {
569 /// Native resource type, if supplied.
570 resource_type: Option<String>,
571 /// Exact native resource name.
572 name: String,
573 /// Native resource owner, if supplied.
574 owner: Option<String>,
575 /// Native description, if supplied.
576 description: Option<String>,
577 },
578 /// Native request field validation failure.
579 FieldViolation {
580 /// Native field path.
581 field: String,
582 /// Native reason, when supplied.
583 reason: Option<String>,
584 /// Native diagnostic prose, when supplied.
585 description: Option<String>,
586 },
587 /// Native retry guidance; it does not authorize an automatic retry.
588 RetryDelay {
589 /// Whole nonnegative seconds.
590 seconds: u64,
591 /// Reported fractional nanoseconds.
592 nanos: u32,
593 },
594 /// Native quota violation.
595 QuotaViolation {
596 /// Native quota subject, when supplied.
597 subject: Option<String>,
598 /// Native diagnostic prose, when supplied.
599 description: Option<String>,
600 },
601 /// Native precondition failure.
602 PreconditionViolation {
603 /// Native precondition kind, when supplied.
604 kind: Option<String>,
605 /// Native subject, when supplied.
606 subject: Option<String>,
607 /// Native diagnostic prose, when supplied.
608 description: Option<String>,
609 },
610}
611
612impl NativeDetail {
613 fn strings(&self) -> [Option<&str>; 4] {
614 match self {
615 Self::Reason { domain, reason } => [Some(domain), Some(reason), None, None],
616 Self::Resource {
617 resource_type,
618 name,
619 owner,
620 description,
621 } => [
622 resource_type.as_deref(),
623 Some(name),
624 owner.as_deref(),
625 description.as_deref(),
626 ],
627 Self::FieldViolation {
628 field,
629 reason,
630 description,
631 } => [Some(field), reason.as_deref(), description.as_deref(), None],
632 Self::QuotaViolation {
633 subject,
634 description,
635 } => [subject.as_deref(), description.as_deref(), None, None],
636 Self::PreconditionViolation {
637 kind,
638 subject,
639 description,
640 } => [
641 kind.as_deref(),
642 subject.as_deref(),
643 description.as_deref(),
644 None,
645 ],
646 Self::RetryDelay { .. } => [None, None, None, None],
647 }
648 }
649
650 fn bound(&mut self, budget: &mut DetailBudget) {
651 match self {
652 Self::Reason { domain, reason } => {
653 budget.string(domain);
654 budget.string(reason);
655 }
656 Self::Resource {
657 resource_type,
658 name,
659 owner,
660 description,
661 } => {
662 budget.string(name);
663 for value in [resource_type, owner, description].into_iter().flatten() {
664 budget.string(value);
665 }
666 }
667 Self::FieldViolation {
668 field,
669 reason,
670 description,
671 } => {
672 budget.string(field);
673 for value in [reason, description].into_iter().flatten() {
674 budget.string(value);
675 }
676 }
677 Self::QuotaViolation {
678 subject,
679 description,
680 } => {
681 for value in [subject, description].into_iter().flatten() {
682 budget.string(value);
683 }
684 }
685 Self::PreconditionViolation {
686 kind,
687 subject,
688 description,
689 } => {
690 for value in [kind, subject, description].into_iter().flatten() {
691 budget.string(value);
692 }
693 }
694 Self::RetryDelay { .. } => {}
695 }
696 }
697}
698
699impl fmt::Debug for NativeDetail {
700 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
701 let name = match self {
702 Self::Reason { .. } => "Reason",
703 Self::Resource { .. } => "Resource",
704 Self::FieldViolation { .. } => "FieldViolation",
705 Self::RetryDelay { .. } => "RetryDelay",
706 Self::QuotaViolation { .. } => "QuotaViolation",
707 Self::PreconditionViolation { .. } => "PreconditionViolation",
708 };
709 formatter.write_str(name)?;
710 formatter.write_str(" { details: <redacted> }")
711 }
712}
713
714struct DetailBudget {
715 remaining: usize,
716 remaining_nodes: usize,
717 truncated: bool,
718}
719
720impl DetailBudget {
721 fn new() -> Self {
722 Self {
723 remaining: DETAIL_TOTAL_LIMIT,
724 remaining_nodes: ERROR_NODE_LIMIT,
725 truncated: false,
726 }
727 }
728
729 fn string(&mut self, value: &mut String) {
730 let limit = DETAIL_STRING_LIMIT.min(self.remaining);
731 if value.len() > limit {
732 let mut end = limit;
733 while !value.is_char_boundary(end) {
734 end -= 1;
735 }
736 value.truncate(end);
737 self.truncated = true;
738 }
739 self.remaining -= value.len();
740 }
741}
742
743/// Potentially sensitive native details, available only through explicit inspection.
744///
745/// ```
746/// use providers_kit::error::NativeErrorDetails;
747/// let details = NativeErrorDetails { message: Some("private payload".into()), ..Default::default() };
748/// assert!(!format!("{details:?}").contains("private payload"));
749/// ```
750///
751/// All string fields are bounded when attached to [`KitError`]. `Debug` deliberately
752/// hides them, including native codes and request IDs, because a service or proxy can
753/// return arbitrary text in any of them. Raw bodies retain provider-specific fields
754/// not covered by the portable metadata vocabulary.
755#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
756#[serde(default, deny_unknown_fields)]
757pub struct NativeErrorDetails {
758 /// Native or Kit diagnostic prose, potentially containing caller input.
759 pub message: Option<String>,
760 /// Bounded native response or frame body, potentially containing secrets.
761 pub body: Option<String>,
762 /// Native error code; its meaning depends on the wire protocol.
763 pub code: Option<String>,
764 /// Native error type, distinct from its code.
765 pub error_type: Option<String>,
766 /// Native event discriminator, distinct from the inner error type.
767 pub event_type: Option<String>,
768 /// Native hosted execution outcome.
769 pub outcome: Option<String>,
770 /// Native parameter associated with the failure.
771 pub parameter: Option<String>,
772 /// Native request correlation ID.
773 pub request_id: Option<String>,
774 /// ID of the server event reporting the failure.
775 pub event_id: Option<String>,
776 /// ID of the client event that caused the failure.
777 pub related_event_id: Option<String>,
778 /// Native tool-call correlation, never a missing-resource identifier.
779 pub tool_call_id: Option<String>,
780 /// Native stream event sequence number, when reported.
781 pub sequence_number: Option<u64>,
782 /// Native status name (for example, a Google RPC canonical status).
783 pub native_status: Option<String>,
784 /// HTTP status reported by a native body or embedded result, not an invented outer status.
785 pub reported_http_status: Option<u16>,
786 /// Native RPC canonical numeric code, distinct from HTTP status.
787 pub rpc_code: Option<i64>,
788 /// Native execution exit code, distinct from HTTP and RPC codes.
789 pub execution_exit_code: Option<i64>,
790 /// Native Retry-After header; no automatic retry is implied.
791 pub retry_after: Option<String>,
792 /// Request URL, potentially containing credentials or private resource names.
793 pub url: Option<String>,
794 /// Repeated structured native evidence; at most 256 records are retained.
795 pub details: Vec<NativeDetail>,
796 /// At least one detail was truncated or the body was known to be incomplete.
797 pub truncated: bool,
798 /// Number of structured native records omitted during bounded capture.
799 pub omitted_details: usize,
800}
801
802impl NativeErrorDetails {
803 fn bound(&mut self) {
804 let mut budget = DetailBudget::new();
805 self.bound_with_budget(&mut budget);
806 }
807
808 fn bound_with_budget(&mut self, budget: &mut DetailBudget) {
809 // Reserve the named classification evidence before optional diagnostics.
810 for value in [
811 &mut self.code,
812 &mut self.error_type,
813 &mut self.native_status,
814 &mut self.outcome,
815 ]
816 .into_iter()
817 .flatten()
818 {
819 budget.string(value);
820 }
821 // The first reason is the one used by a NativeReason classifier. Retain
822 // that record even when it occurs after the general record limit.
823 let reason_index = self
824 .details
825 .iter()
826 .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
827 if let Some(index) = reason_index {
828 self.details[index].bound(budget);
829 if index >= DETAIL_RECORD_LIMIT {
830 let reason = self.details.remove(index);
831 self.details.insert(0, reason);
832 }
833 }
834 let omitted = self.details.len().saturating_sub(DETAIL_RECORD_LIMIT);
835 self.omitted_details = self.omitted_details.saturating_add(omitted);
836 self.details.truncate(DETAIL_RECORD_LIMIT);
837 let retained_reason = self
838 .details
839 .iter()
840 .position(|detail| matches!(detail, NativeDetail::Reason { .. }));
841 for (index, detail) in self.details.iter_mut().enumerate() {
842 if Some(index) != retained_reason {
843 detail.bound(budget);
844 }
845 }
846 for value in [
847 &mut self.event_type,
848 &mut self.parameter,
849 &mut self.request_id,
850 &mut self.event_id,
851 &mut self.related_event_id,
852 &mut self.tool_call_id,
853 &mut self.retry_after,
854 &mut self.url,
855 &mut self.message,
856 &mut self.body,
857 ]
858 .into_iter()
859 .flatten()
860 {
861 budget.string(value);
862 }
863 self.truncated |= budget.truncated || self.omitted_details != 0;
864 }
865
866 fn validate(&self) -> Result<(), KitError> {
867 if self.details.len() > DETAIL_RECORD_LIMIT {
868 return Err(KitError::invalid_input(
869 ValidationViolation::LimitExceeded {
870 field: "native_details.details",
871 limit: DETAIL_RECORD_LIMIT,
872 },
873 "native details exceed 256 records",
874 ));
875 }
876 if self.omitted_details != 0 && !self.truncated {
877 return Err(KitError::invalid_input(
878 ValidationViolation::InvalidValue {
879 field: "native_details.truncated",
880 rule: "required when detail records were omitted",
881 },
882 "omitted native details require a truncation marker",
883 ));
884 }
885 let named = [
886 self.message.as_deref(),
887 self.body.as_deref(),
888 self.code.as_deref(),
889 self.error_type.as_deref(),
890 self.event_type.as_deref(),
891 self.outcome.as_deref(),
892 self.parameter.as_deref(),
893 self.request_id.as_deref(),
894 self.event_id.as_deref(),
895 self.related_event_id.as_deref(),
896 self.tool_call_id.as_deref(),
897 self.native_status.as_deref(),
898 self.retry_after.as_deref(),
899 self.url.as_deref(),
900 ];
901 let strings = named.into_iter().flatten().chain(
902 self.details
903 .iter()
904 .flat_map(|detail| detail.strings().into_iter().flatten()),
905 );
906 let mut total = 0_usize;
907 for value in strings {
908 if value.len() > DETAIL_STRING_LIMIT {
909 return Err(KitError::invalid_input(
910 ValidationViolation::LimitExceeded {
911 field: "native_details.string",
912 limit: DETAIL_STRING_LIMIT,
913 },
914 "native detail exceeds 64 KiB",
915 ));
916 }
917 total = total.saturating_add(value.len());
918 }
919 if total > DETAIL_TOTAL_LIMIT {
920 return Err(KitError::invalid_input(
921 ValidationViolation::LimitExceeded {
922 field: "native_details",
923 limit: DETAIL_TOTAL_LIMIT,
924 },
925 "native details exceed 1 MiB in total",
926 ));
927 }
928 Ok(())
929 }
930}
931
932impl fmt::Debug for NativeErrorDetails {
933 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
934 formatter
935 .debug_struct("NativeErrorDetails")
936 .field("details", &"<redacted>")
937 .field("truncated", &self.truncated)
938 .field("omitted_details", &self.omitted_details)
939 .finish()
940 }
941}
942
943#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
944#[serde(rename_all = "snake_case")]
945enum RemoteCategory {
946 Unsupported,
947 InvalidRequest,
948 Authentication,
949 PermissionDenied,
950 NotFound,
951 Conflict,
952 RequestTooLarge,
953 FailedPrecondition,
954 ResourceExhausted,
955 RateLimited,
956 QuotaExceeded,
957 Billing,
958 ServiceUnavailable,
959 Internal,
960 DeadlineExceeded,
961 Cancelled,
962 ProviderRejected,
963}
964
965impl RemoteCategory {
966 fn from_kind(kind: ErrorKind) -> Result<Self, KitError> {
967 match kind {
968 ErrorKind::Unsupported => Ok(Self::Unsupported),
969 ErrorKind::InvalidRequest => Ok(Self::InvalidRequest),
970 ErrorKind::Authentication => Ok(Self::Authentication),
971 ErrorKind::PermissionDenied => Ok(Self::PermissionDenied),
972 ErrorKind::NotFound => Ok(Self::NotFound),
973 ErrorKind::Conflict => Ok(Self::Conflict),
974 ErrorKind::RequestTooLarge => Ok(Self::RequestTooLarge),
975 ErrorKind::FailedPrecondition => Ok(Self::FailedPrecondition),
976 ErrorKind::ResourceExhausted => Ok(Self::ResourceExhausted),
977 ErrorKind::RateLimited => Ok(Self::RateLimited),
978 ErrorKind::QuotaExceeded => Ok(Self::QuotaExceeded),
979 ErrorKind::Billing => Ok(Self::Billing),
980 ErrorKind::ServiceUnavailable => Ok(Self::ServiceUnavailable),
981 ErrorKind::Internal => Ok(Self::Internal),
982 ErrorKind::DeadlineExceeded => Ok(Self::DeadlineExceeded),
983 ErrorKind::Cancelled => Ok(Self::Cancelled),
984 ErrorKind::ProviderRejected => Ok(Self::ProviderRejected),
985 ErrorKind::Configuration
986 | ErrorKind::InvalidInput
987 | ErrorKind::Transport
988 | ErrorKind::Timeout
989 | ErrorKind::Closed
990 | ErrorKind::InvalidState
991 | ErrorKind::InvalidResponse(_) => Err(KitError::invalid_input(
992 ValidationViolation::InvalidValue {
993 field: "remote_failure.kind",
994 rule: "must classify a remote outcome",
995 },
996 "remote failure cannot use a local or response-schema classification",
997 )),
998 }
999 }
1000
1001 fn kind(self) -> ErrorKind {
1002 match self {
1003 Self::Unsupported => ErrorKind::Unsupported,
1004 Self::InvalidRequest => ErrorKind::InvalidRequest,
1005 Self::Authentication => ErrorKind::Authentication,
1006 Self::PermissionDenied => ErrorKind::PermissionDenied,
1007 Self::NotFound => ErrorKind::NotFound,
1008 Self::Conflict => ErrorKind::Conflict,
1009 Self::RequestTooLarge => ErrorKind::RequestTooLarge,
1010 Self::FailedPrecondition => ErrorKind::FailedPrecondition,
1011 Self::ResourceExhausted => ErrorKind::ResourceExhausted,
1012 Self::RateLimited => ErrorKind::RateLimited,
1013 Self::QuotaExceeded => ErrorKind::QuotaExceeded,
1014 Self::Billing => ErrorKind::Billing,
1015 Self::ServiceUnavailable => ErrorKind::ServiceUnavailable,
1016 Self::Internal => ErrorKind::Internal,
1017 Self::DeadlineExceeded => ErrorKind::DeadlineExceeded,
1018 Self::Cancelled => ErrorKind::Cancelled,
1019 Self::ProviderRejected => ErrorKind::ProviderRejected,
1020 }
1021 }
1022}
1023
1024/// An opaque remote failure, including failures inside successful batch or hosted results.
1025///
1026/// Serialization deliberately exports all retained native evidence, including the
1027/// bounded body. It is not a redacted logging format. Deserialization rejects invalid
1028/// categories, inconsistent evidence and values exceeding the documented bounds.
1029///
1030/// ```
1031/// use providers_kit::error::{ErrorKind, NativeErrorDetails, RemoteFailure};
1032/// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails::default())?;
1033/// assert_eq!(failure.kind(), ErrorKind::NotFound);
1034///
1035/// # Ok::<(), providers_kit::KitError>(())
1036/// ```
1037#[derive(Clone, Eq, PartialEq)]
1038pub struct RemoteFailure {
1039 category: RemoteCategory,
1040 basis: ClassificationBasis,
1041 details: Box<NativeErrorDetails>,
1042}
1043
1044impl RemoteFailure {
1045 /// Constructs an explicitly classified caller failure without claiming native inference.
1046 ///
1047 /// Details must already fit 64 KiB per string, 1 MiB total string bytes, and 256
1048 /// repeated records. Oversized caller evidence is rejected rather than discarded.
1049 ///
1050 /// # Errors
1051 ///
1052 /// Returns an input error for local-only categories, invalid numeric/detail fields, or evidence exceeding the documented capture bounds. Explicit classification does not authenticate origin or establish retry safety.
1053 ///
1054 /// # Examples
1055 ///
1056 /// ```
1057 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1058 /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1059 /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1060 /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1061 /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1062 /// assert!(!format!("{failure:?}").contains("private detail"));
1063 ///
1064 /// # Ok::<(), providers_kit::KitError>(())
1065 /// ```
1066 ///
1067 pub fn new(kind: ErrorKind, details: NativeErrorDetails) -> Result<Self, KitError> {
1068 let category = RemoteCategory::from_kind(kind)?;
1069 details.validate()?;
1070 Ok(Self {
1071 category,
1072 basis: ClassificationBasis::Explicit,
1073 details: Box::new(details),
1074 })
1075 }
1076
1077 /// Returns the same portable classification vocabulary used by [`KitError`].
1078 ///
1079 /// # Examples
1080 ///
1081 /// ```
1082 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1083 /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1084 /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1085 /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1086 /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1087 /// assert!(!format!("{failure:?}").contains("private detail"));
1088 ///
1089 /// # Ok::<(), providers_kit::KitError>(())
1090 /// ```
1091 ///
1092 pub fn kind(&self) -> ErrorKind {
1093 self.category.kind()
1094 }
1095
1096 /// Returns the evidence used for classification, independently of the retained body.
1097 ///
1098 /// # Examples
1099 ///
1100 /// ```
1101 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1102 /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1103 /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1104 /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1105 /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1106 /// assert!(!format!("{failure:?}").contains("private detail"));
1107 ///
1108 /// # Ok::<(), providers_kit::KitError>(())
1109 /// ```
1110 ///
1111 pub fn classification_basis(&self) -> ClassificationBasis {
1112 self.basis
1113 }
1114
1115 /// Explicitly exposes potentially sensitive native evidence.
1116 ///
1117 /// # Examples
1118 ///
1119 /// ```
1120 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1121 /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1122 /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1123 /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1124 /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1125 /// assert!(!format!("{failure:?}").contains("private detail"));
1126 ///
1127 /// # Ok::<(), providers_kit::KitError>(())
1128 /// ```
1129 ///
1130 pub fn native_details(&self) -> &NativeErrorDetails {
1131 &self.details
1132 }
1133
1134 pub(crate) fn from_native(
1135 kind: ErrorKind,
1136 basis: ClassificationBasis,
1137 mut details: NativeErrorDetails,
1138 ) -> Result<Self, KitError> {
1139 let category = RemoteCategory::from_kind(kind)?;
1140 details.bound();
1141 let failure = Self {
1142 category,
1143 basis,
1144 details: Box::new(details),
1145 };
1146 failure.validate()?;
1147 Ok(failure)
1148 }
1149
1150 fn validate(&self) -> Result<(), KitError> {
1151 self.details.validate()?;
1152 let present = match self.basis {
1153 ClassificationBasis::Explicit => true,
1154 ClassificationBasis::HttpStatus { status } => {
1155 (100..=599).contains(&status)
1156 && !(200..300).contains(&status)
1157 && self.kind() == ErrorKind::from_http_status(status)
1158 }
1159 ClassificationBasis::NativeCode => self
1160 .details
1161 .code
1162 .as_deref()
1163 .is_some_and(|value| !value.trim().is_empty()),
1164 ClassificationBasis::NativeErrorType => self
1165 .details
1166 .error_type
1167 .as_deref()
1168 .is_some_and(|value| !value.trim().is_empty()),
1169 ClassificationBasis::CanonicalStatus => {
1170 self.details.native_status.as_deref().is_some_and(|value| {
1171 let kind = ErrorKind::from_canonical_status(Some(value));
1172 kind != ErrorKind::ProviderRejected && self.kind() == kind
1173 })
1174 }
1175 ClassificationBasis::RpcCode => self.details.rpc_code.is_some_and(|code| {
1176 let kind = ErrorKind::from_rpc_code(Some(code));
1177 kind != ErrorKind::ProviderRejected && self.kind() == kind
1178 }),
1179 ClassificationBasis::NativeReason => self
1180 .details
1181 .details
1182 .iter()
1183 .find_map(|detail| match detail {
1184 NativeDetail::Reason { domain, reason } => {
1185 Some(!domain.trim().is_empty() && !reason.trim().is_empty())
1186 }
1187 _ => None,
1188 })
1189 .unwrap_or(false),
1190 ClassificationBasis::NativeOutcome => match self.details.outcome.as_deref() {
1191 Some("OUTCOME_FAILED" | "URL_RETRIEVAL_STATUS_ERROR") => {
1192 self.kind() == ErrorKind::ProviderRejected
1193 }
1194 Some("OUTCOME_DEADLINE_EXCEEDED") => self.kind() == ErrorKind::DeadlineExceeded,
1195 Some("URL_RETRIEVAL_STATUS_PAYWALL" | "URL_RETRIEVAL_STATUS_UNSAFE") => {
1196 self.kind() == ErrorKind::PermissionDenied
1197 }
1198 None => {
1199 self.details
1200 .execution_exit_code
1201 .is_some_and(|code| code != 0)
1202 && self.kind() == ErrorKind::ProviderRejected
1203 }
1204 Some(_) => false,
1205 },
1206 ClassificationBasis::Unclassified => self.category == RemoteCategory::ProviderRejected,
1207 };
1208 if !present {
1209 return Err(KitError::invalid_input(
1210 ValidationViolation::InvalidValue {
1211 field: "remote_failure.basis",
1212 rule: "requires corresponding classification evidence",
1213 },
1214 "remote classification basis does not have corresponding evidence",
1215 ));
1216 }
1217 Ok(())
1218 }
1219}
1220
1221impl Serialize for RemoteFailure {
1222 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1223 where
1224 S: serde::Serializer,
1225 {
1226 #[derive(Serialize)]
1227 struct Wire<'a> {
1228 kind: RemoteCategory,
1229 basis: ClassificationBasis,
1230 details: &'a NativeErrorDetails,
1231 }
1232 Wire {
1233 kind: self.category,
1234 basis: self.basis,
1235 details: &self.details,
1236 }
1237 .serialize(serializer)
1238 }
1239}
1240
1241impl<'de> Deserialize<'de> for RemoteFailure {
1242 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1243 where
1244 D: serde::Deserializer<'de>,
1245 {
1246 #[derive(Deserialize)]
1247 #[serde(deny_unknown_fields)]
1248 struct Wire {
1249 kind: RemoteCategory,
1250 basis: ClassificationBasis,
1251 details: NativeErrorDetails,
1252 }
1253 let wire = Wire::deserialize(deserializer)?;
1254 let failure = Self {
1255 category: wire.kind,
1256 basis: wire.basis,
1257 details: Box::new(wire.details),
1258 };
1259 failure.validate().map_err(serde::de::Error::custom)?;
1260 Ok(failure)
1261 }
1262}
1263
1264impl fmt::Display for RemoteFailure {
1265 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1266 write!(formatter, "remote {:?} ({:?})", self.kind(), self.basis)
1267 }
1268}
1269
1270impl fmt::Debug for RemoteFailure {
1271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1272 formatter
1273 .debug_struct("RemoteFailure")
1274 .field("kind", &self.kind())
1275 .field("basis", &self.basis)
1276 .field("details", &self.details)
1277 .finish()
1278 }
1279}
1280
1281impl std::error::Error for RemoteFailure {}
1282
1283#[derive(Clone)]
1284enum ErrorFailure {
1285 Classified {
1286 kind: ErrorKind,
1287 details: Box<NativeErrorDetails>,
1288 },
1289 Remote(RemoteFailure),
1290}
1291
1292impl ErrorFailure {
1293 fn kind(&self) -> ErrorKind {
1294 match self {
1295 Self::Classified { kind, .. } => *kind,
1296 Self::Remote(failure) => failure.kind(),
1297 }
1298 }
1299
1300 fn details(&self) -> &NativeErrorDetails {
1301 match self {
1302 Self::Classified { details, .. } => details,
1303 Self::Remote(failure) => &failure.details,
1304 }
1305 }
1306
1307 fn details_mut(&mut self) -> &mut NativeErrorDetails {
1308 match self {
1309 Self::Classified { details, .. } => details,
1310 Self::Remote(failure) => &mut failure.details,
1311 }
1312 }
1313}
1314
1315#[derive(Clone)]
1316struct ErrorData {
1317 failure: ErrorFailure,
1318 validation: Option<ValidationViolation>,
1319 truncated: bool,
1320 status: Option<u16>,
1321 timeout: Option<Duration>,
1322 response: Option<ResponseDetails>,
1323 request_target: Option<String>,
1324 correlation_id: Option<String>,
1325 cause: Option<Box<KitError>>,
1326 supplementary: Vec<KitError>,
1327}
1328
1329impl ErrorData {
1330 fn bound(&mut self) {
1331 self.bound_tree(&mut DetailBudget::new(), 1);
1332 }
1333
1334 fn bound_tree(&mut self, budget: &mut DetailBudget, depth: usize) {
1335 budget.remaining_nodes -= 1;
1336 let previous_truncation = budget.truncated;
1337 budget.truncated = false;
1338 // Retain this node's observations and local correlation before its native body.
1339 if let Some(response) = self.response.as_mut() {
1340 response.bound(budget);
1341 }
1342 for value in [&mut self.request_target, &mut self.correlation_id]
1343 .into_iter()
1344 .flatten()
1345 {
1346 budget.string(value);
1347 }
1348 self.failure.details_mut().bound_with_budget(budget);
1349 self.truncated |= self.failure.details().truncated;
1350 if depth == ERROR_DEPTH_LIMIT {
1351 self.truncated |= self.cause.is_some() || !self.supplementary.is_empty();
1352 self.cause = None;
1353 self.supplementary.clear();
1354 } else {
1355 if self.cause.as_ref().is_some_and(|cause| {
1356 budget.remaining_nodes == 0 || !cause.data.retains_remote_evidence(budget)
1357 }) {
1358 self.cause = None;
1359 self.truncated = true;
1360 } else if let Some(cause) = self.cause.as_mut() {
1361 cause.data.bound_tree(budget, depth + 1);
1362 }
1363 let mut omitted = false;
1364 self.supplementary.retain_mut(|child| {
1365 if budget.remaining_nodes == 0 || !child.data.retains_remote_evidence(budget) {
1366 omitted = true;
1367 false
1368 } else {
1369 child.data.bound_tree(budget, depth + 1);
1370 true
1371 }
1372 });
1373 self.truncated |= omitted;
1374 }
1375 self.truncated |= budget.truncated;
1376 budget.truncated |= previous_truncation || self.truncated;
1377 }
1378
1379 fn retains_remote_evidence(&self, available: &DetailBudget) -> bool {
1380 let ErrorFailure::Remote(failure) = &self.failure else {
1381 return true;
1382 };
1383 let mut budget = DetailBudget {
1384 remaining: available.remaining,
1385 remaining_nodes: available.remaining_nodes,
1386 truncated: false,
1387 };
1388 if let Some(mut response) = self.response.clone() {
1389 response.bound(&mut budget);
1390 }
1391 for value in [&self.request_target, &self.correlation_id]
1392 .into_iter()
1393 .flatten()
1394 {
1395 budget.string(&mut value.clone());
1396 }
1397 let mut retained = failure.clone();
1398 retained.details.bound_with_budget(&mut budget);
1399 // A child is optional diagnostic context. Omit its subtree instead of
1400 // exposing an invalid serializable RemoteFailure or changing its basis.
1401 retained.validate().is_ok()
1402 }
1403}
1404
1405/// Structured error returned by the portable provider API.
1406///
1407/// ```
1408/// use providers_kit::KitError;
1409/// use providers_kit::error::{ErrorKind, ErrorPhase, Operation, ResponseViolation};
1410/// let error = KitError::new(
1411/// ErrorKind::InvalidResponse(ResponseViolation::MissingField { field: "modelVersion" }),
1412/// Operation::BatchResults,
1413/// ErrorPhase::Normalize,
1414/// ).with_row(0);
1415/// assert_eq!(error.context().row, Some(0));
1416/// assert!(error.to_string().contains("modelVersion"));
1417/// ```
1418///
1419/// Runtime errors are deliberately not serializable. Accepted remote outcomes use
1420/// [`RemoteFailure`]; runtime errors may contain trusted static schema labels and
1421/// causal Kit failures that must not be reconstructed from untrusted JSON.
1422///
1423/// ```compile_fail
1424/// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1425/// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::RequestSend);
1426/// let _ = serde_json::to_string(&error);
1427/// ```
1428#[derive(Clone)]
1429pub struct KitError {
1430 context: ErrorContext,
1431 data: Box<ErrorData>,
1432}
1433
1434impl KitError {
1435 /// Constructs a classified failure without native details.
1436 ///
1437 /// ```
1438 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1439 /// let error = KitError::new(ErrorKind::Unsupported, Operation::Embed, ErrorPhase::InputValidation);
1440 /// assert_eq!(error.kind(), ErrorKind::Unsupported);
1441 /// ```
1442 pub fn new(kind: ErrorKind, operation: Operation, phase: ErrorPhase) -> Self {
1443 Self {
1444 context: ErrorContext {
1445 operation,
1446 phase,
1447 row: None,
1448 },
1449 data: Box::new(ErrorData {
1450 failure: ErrorFailure::Classified {
1451 kind,
1452 details: Box::default(),
1453 },
1454 validation: None,
1455 truncated: false,
1456 status: None,
1457 timeout: None,
1458 response: None,
1459 request_target: None,
1460 correlation_id: None,
1461 cause: None,
1462 supplementary: Vec::new(),
1463 }),
1464 }
1465 }
1466
1467 /// Attaches a remote failure to the operation that observed it.
1468 ///
1469 /// This does not invent an outer HTTP status from an embedded result or stream.
1470 ///
1471 /// # Examples
1472 ///
1473 /// ```
1474 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1475 /// let failure = RemoteFailure::new(ErrorKind::NotFound, NativeErrorDetails { message: Some("private detail".into()), ..Default::default() })?;
1476 /// use providers_kit::error::{KitError, Operation, ErrorPhase};
1477 /// let error = KitError::from_remote(failure, Operation::FileInfo, ErrorPhase::ReadBody);
1478 /// assert_eq!(error.kind(), ErrorKind::NotFound);
1479 ///
1480 /// # Ok::<(), providers_kit::KitError>(())
1481 /// ```
1482 ///
1483 pub fn from_remote(failure: RemoteFailure, operation: Operation, phase: ErrorPhase) -> Self {
1484 Self::new(failure.kind(), operation, phase).with_remote_failure(failure)
1485 }
1486
1487 pub(crate) fn with_remote_failure(mut self, failure: RemoteFailure) -> Self {
1488 self.data.failure = ErrorFailure::Remote(failure);
1489 self.data.bound();
1490 self
1491 }
1492
1493 /// Returns the basis for classification; caller construction remains explicit.
1494 ///
1495 /// # Examples
1496 ///
1497 /// ```
1498 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1499 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1500 /// let _basis = error.classification_basis();
1501 /// ```
1502 ///
1503 pub fn classification_basis(&self) -> ClassificationBasis {
1504 match &self.data.failure {
1505 ErrorFailure::Classified { .. } => ClassificationBasis::Explicit,
1506 ErrorFailure::Remote(failure) => failure.classification_basis(),
1507 }
1508 }
1509
1510 /// Returns an attached remote failure, if one was represented explicitly.
1511 ///
1512 /// Absence does not establish that the implementation made no service request.
1513 ///
1514 /// # Examples
1515 ///
1516 /// ```
1517 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1518 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1519 /// assert!(error.remote_failure().is_none());
1520 /// ```
1521 ///
1522 pub fn remote_failure(&self) -> Option<&RemoteFailure> {
1523 match &self.data.failure {
1524 ErrorFailure::Classified { .. } => None,
1525 ErrorFailure::Remote(failure) => Some(failure),
1526 }
1527 }
1528
1529 /// Explicitly exposes potentially sensitive observations for a response violation.
1530 ///
1531 /// # Examples
1532 ///
1533 /// ```
1534 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1535 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1536 /// assert!(error.response_details().is_none());
1537 /// ```
1538 ///
1539 pub fn response_details(&self) -> Option<&ResponseDetails> {
1540 self.data.response.as_ref()
1541 }
1542
1543 /// Returns the safe, detector-assigned local input invariant, when applicable.
1544 ///
1545 /// ```
1546 /// use providers_kit::{generation::GenerationConfig, error::ValidationViolation};
1547 /// let error = GenerationConfig::default().with_temperature(-1.0).unwrap_err();
1548 /// assert!(matches!(error.validation_violation(), Some(ValidationViolation::OutOfRange {
1549 /// field: "generation.temperature", ..
1550 /// })));
1551 /// ```
1552 pub fn validation_violation(&self) -> Option<&ValidationViolation> {
1553 self.data.validation.as_ref()
1554 }
1555
1556 /// Attaches trusted field/rule labels without exposing rejected caller values.
1557 ///
1558 /// ```
1559 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation, ValidationViolation}};
1560 /// let error = KitError::new(ErrorKind::InvalidInput, Operation::ValidateInput, ErrorPhase::InputValidation)
1561 /// .with_validation_violation(ValidationViolation::Empty { field: "messages" });
1562 /// assert_eq!(error.validation_violation(), Some(&ValidationViolation::Empty { field: "messages" }));
1563 /// ```
1564 pub fn with_validation_violation(mut self, violation: ValidationViolation) -> Self {
1565 self.data.validation = Some(violation);
1566 self
1567 }
1568
1569 /// Reports whether this tree lost diagnostics to string, record, depth, or node bounds.
1570 ///
1571 /// A tree retains at most eight levels, 32 nodes, and 1 MiB of diagnostic strings.
1572 /// The primary error is retained before its cause subtree and supplementary errors.
1573 ///
1574 /// ```
1575 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, NativeErrorDetails, Operation}};
1576 /// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody)
1577 /// .with_native_details(NativeErrorDetails {
1578 /// message: Some("x".repeat(65_537)), ..Default::default()
1579 /// });
1580 /// assert!(error.diagnostics_truncated());
1581 /// ```
1582 pub fn diagnostics_truncated(&self) -> bool {
1583 self.data.truncated
1584 }
1585
1586 /// Returns the cause of this failure, separately from cleanup or diagnostic failures.
1587 ///
1588 /// # Examples
1589 ///
1590 /// ```
1591 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1592 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1593 /// assert!(error.cause().is_none());
1594 /// ```
1595 ///
1596 pub fn cause(&self) -> Option<&Self> {
1597 self.data.cause.as_deref()
1598 }
1599
1600 /// Explicitly exposes the local requested target; this is not native attribution.
1601 ///
1602 /// # Examples
1603 ///
1604 /// ```
1605 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1606 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1607 /// assert!(error.request_target().is_none());
1608 /// ```
1609 ///
1610 pub fn request_target(&self) -> Option<&str> {
1611 self.data.request_target.as_deref()
1612 }
1613
1614 /// Explicitly exposes caller correlation, such as a batch custom ID.
1615 ///
1616 /// # Examples
1617 ///
1618 /// ```
1619 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1620 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1621 /// assert!(error.correlation_id().is_none());
1622 /// ```
1623 ///
1624 pub fn correlation_id(&self) -> Option<&str> {
1625 self.data.correlation_id.as_deref()
1626 }
1627
1628 /// Retains response observations without placing native values in trusted labels.
1629 ///
1630 /// # Examples
1631 ///
1632 /// ```
1633 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1634 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1635 /// let details = providers_kit::error::ResponseDetails::Json { category: providers_kit::error::JsonErrorCategory::Syntax, line: 1, column: 2 };
1636 /// let error = error.with_response_details(details);
1637 /// assert!(error.response_details().is_some());
1638 /// ```
1639 ///
1640 pub fn with_response_details(mut self, details: ResponseDetails) -> Self {
1641 self.data.response = Some(details);
1642 self.data.bound();
1643 self
1644 }
1645
1646 /// Retains an actual causal failure. Independent cleanup belongs in supplementary errors.
1647 ///
1648 /// # Examples
1649 ///
1650 /// ```
1651 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1652 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1653 /// let cause = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1654 /// let error = error.with_cause(cause);
1655 /// assert_eq!(error.cause().unwrap().kind(), ErrorKind::Timeout);
1656 /// ```
1657 ///
1658 pub fn with_cause(mut self, cause: Self) -> Self {
1659 self.data.cause = Some(Box::new(cause));
1660 self.data.bound();
1661 self
1662 }
1663
1664 /// Retains a bounded local target without asserting that the provider identified it.
1665 ///
1666 /// # Examples
1667 ///
1668 /// ```
1669 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1670 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1671 /// let error = error.with_request_target("private-resource");
1672 /// assert_eq!(error.request_target(), Some("private-resource"));
1673 /// assert!(!format!("{error}").contains("private-resource"));
1674 /// ```
1675 ///
1676 pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
1677 self.data.request_target = Some(target.into());
1678 self.data.bound();
1679 self
1680 }
1681
1682 /// Retains bounded caller correlation without treating it as a missing-resource identity.
1683 ///
1684 /// # Examples
1685 ///
1686 /// ```
1687 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1688 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1689 /// let error = error.with_correlation_id("row-1");
1690 /// assert_eq!(error.correlation_id(), Some("row-1"));
1691 /// ```
1692 ///
1693 pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
1694 self.data.correlation_id = Some(id.into());
1695 self.data.bound();
1696 self
1697 }
1698
1699 /// Returns the portable classification without interpreting native prose.
1700 ///
1701 /// ```
1702 ///
1703 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1704 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1705 /// assert_eq!(error.kind(), ErrorKind::Transport);
1706 /// ```
1707 pub fn kind(&self) -> ErrorKind {
1708 self.data.failure.kind()
1709 }
1710 /// Returns safe operation, phase and batch row context.
1711 ///
1712 /// ```
1713 ///
1714 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1715 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1716 /// assert_eq!(error.context().operation, Operation::Generate);
1717 /// ```
1718 pub fn context(&self) -> &ErrorContext {
1719 &self.context
1720 }
1721 /// Returns the actual unsuccessful HTTP status, if one was received.
1722 ///
1723 /// ```
1724 ///
1725 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1726 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1727 /// assert_eq!(error.http_status(), None);
1728 /// ```
1729 pub fn http_status(&self) -> Option<u16> {
1730 self.data.status
1731 }
1732 /// Returns the local deadline that expired, if available.
1733 ///
1734 /// ```
1735 ///
1736 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1737 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1738 /// assert_eq!(error.timeout(), None);
1739 /// ```
1740 pub fn timeout(&self) -> Option<Duration> {
1741 self.data.timeout
1742 }
1743 /// Explicitly exposes potentially sensitive native details; do not log them by default.
1744 ///
1745 /// ```
1746 ///
1747 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1748 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1749 /// assert!(error.native_details().message.is_none());
1750 /// ```
1751 pub fn native_details(&self) -> &NativeErrorDetails {
1752 self.data.failure.details()
1753 }
1754 /// Returns failures collecting diagnostics or releasing resources after the primary failure.
1755 ///
1756 /// ```
1757 ///
1758 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1759 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1760 /// assert!(error.supplementary().is_empty());
1761 /// ```
1762 pub fn supplementary(&self) -> &[KitError] {
1763 &self.data.supplementary
1764 }
1765
1766 /// Attaches an observed HTTP status, never a synthetic status inferred from an event code.
1767 ///
1768 /// ```
1769 ///
1770 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1771 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1772 /// let error = error.with_http_status(503);
1773 /// assert_eq!(error.http_status(), Some(503));
1774 /// ```
1775 pub fn with_http_status(mut self, status: u16) -> Self {
1776 self.data.status = Some(status);
1777 self
1778 }
1779 /// Attaches a local deadline; service timeout responses retain their native classification.
1780 ///
1781 /// ```
1782 ///
1783 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1784 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1785 /// let error = error.with_timeout(std::time::Duration::from_secs(10));
1786 /// assert_eq!(error.timeout(), Some(std::time::Duration::from_secs(10)));
1787 /// ```
1788 pub fn with_timeout(mut self, timeout: Duration) -> Self {
1789 self.data.timeout = Some(timeout);
1790 self
1791 }
1792 /// Attaches the zero-based logical batch row, excluding blank JSONL lines.
1793 ///
1794 /// ```
1795 ///
1796 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1797 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1798 /// let error = error.with_row(2);
1799 /// assert_eq!(error.context().row, Some(2));
1800 /// ```
1801 pub fn with_row(mut self, row: usize) -> Self {
1802 self.context.row = Some(row);
1803 self
1804 }
1805 /// Attaches explicit native details, bounding each string to 64 KiB on UTF-8 boundaries.
1806 ///
1807 /// ```
1808 ///
1809 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1810 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1811 /// let details = providers_kit::error::NativeErrorDetails {
1812 /// request_id: Some("sensitive-correlation-id".into()),
1813 /// ..Default::default()
1814 /// };
1815 /// let error = error.with_native_details(details);
1816 /// assert!(!error.to_string().contains("sensitive-correlation-id"));
1817 /// ```
1818 pub fn with_native_details(mut self, mut details: NativeErrorDetails) -> Self {
1819 details.bound();
1820 // Replacing classification evidence is explicit caller construction; it
1821 // must not preserve a stale adapter-derived basis over unrelated details.
1822 match &mut self.data.failure {
1823 ErrorFailure::Classified {
1824 details: current, ..
1825 } => **current = details,
1826 ErrorFailure::Remote(failure) => {
1827 *failure.details = details;
1828 failure.basis = ClassificationBasis::Explicit;
1829 }
1830 }
1831 self.data.bound();
1832 self
1833 }
1834 /// Preserves a secondary failure without replacing this failure's classification or context.
1835 ///
1836 /// ```
1837 ///
1838 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1839 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1840 /// let secondary = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1841 /// let error = error.with_supplementary(secondary);
1842 /// assert_eq!(error.kind(), ErrorKind::Transport);
1843 /// assert_eq!(error.supplementary()[0].kind(), ErrorKind::Timeout);
1844 /// ```
1845 pub fn with_supplementary(mut self, error: Self) -> Self {
1846 self.data.supplementary.push(error);
1847 self.data.bound();
1848 self
1849 }
1850
1851 pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
1852 self.context.operation = operation;
1853 if let Some(cause) = self.data.cause.take() {
1854 self.data.cause = Some(Box::new(
1855 if cause.context.operation == Operation::Unspecified {
1856 cause.at_operation(operation)
1857 } else {
1858 *cause
1859 },
1860 ));
1861 }
1862 self.data.supplementary = self
1863 .data
1864 .supplementary
1865 .into_iter()
1866 .map(|error| {
1867 if error.context.operation == Operation::Unspecified {
1868 error.at_operation(operation)
1869 } else {
1870 error
1871 }
1872 })
1873 .collect();
1874 self
1875 }
1876 pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
1877 self.context.phase = phase;
1878 self
1879 }
1880 pub(crate) fn with_message(mut self, message: impl Into<String>) -> Self {
1881 self.data.failure.details_mut().message = Some(message.into());
1882 self.data.bound();
1883 self
1884 }
1885 pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
1886 self.data.failure.details_mut().body = body;
1887 self.data.bound();
1888 self
1889 }
1890 pub(crate) fn with_diagnostic(mut self, details: Option<NativeErrorDetails>) -> Self {
1891 if let Some(details) = details {
1892 self.data.failure.details_mut().body = details.body;
1893 self.data.failure.details_mut().truncated |= details.truncated;
1894 }
1895 self.data.bound();
1896 self
1897 }
1898 // Adapter-only enrichment preserves the already-validated native basis.
1899 pub(crate) fn with_native_capture(mut self, details: NativeErrorDetails) -> Self {
1900 *self.data.failure.details_mut() = details;
1901 self.data.bound();
1902 self
1903 }
1904 pub(crate) fn config(violation: ValidationViolation, message: impl Into<String>) -> Self {
1905 Self::new(
1906 ErrorKind::Configuration,
1907 Operation::Configure,
1908 ErrorPhase::Configuration,
1909 )
1910 .with_validation_violation(violation)
1911 .with_message(message)
1912 }
1913 pub(crate) fn invalid_input(
1914 violation: ValidationViolation,
1915 message: impl Into<String>,
1916 ) -> Self {
1917 Self::new(
1918 ErrorKind::InvalidInput,
1919 Operation::ValidateInput,
1920 ErrorPhase::InputValidation,
1921 )
1922 .with_validation_violation(violation)
1923 .with_message(message)
1924 }
1925 pub(crate) fn invalid_response(reason: ResponseViolation) -> Self {
1926 Self::new(
1927 ErrorKind::InvalidResponse(reason),
1928 Operation::Unspecified,
1929 ErrorPhase::Normalize,
1930 )
1931 }
1932 pub(crate) fn unsupported(violation: ValidationViolation, message: impl Into<String>) -> Self {
1933 Self::new(
1934 ErrorKind::Unsupported,
1935 Operation::Unspecified,
1936 ErrorPhase::InputValidation,
1937 )
1938 .with_validation_violation(violation)
1939 .with_message(message)
1940 }
1941 pub(crate) fn invalid_state(message: impl Into<String>) -> Self {
1942 Self::new(
1943 ErrorKind::InvalidState,
1944 Operation::Unspecified,
1945 ErrorPhase::Lifecycle,
1946 )
1947 .with_message(message)
1948 }
1949 pub(crate) fn closed(message: impl Into<String>) -> Self {
1950 Self::new(
1951 ErrorKind::Closed,
1952 Operation::Unspecified,
1953 ErrorPhase::Lifecycle,
1954 )
1955 .with_message(message)
1956 }
1957}
1958
1959impl fmt::Display for KitError {
1960 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1961 write!(
1962 formatter,
1963 "{:?} during {:?}/{:?}",
1964 self.kind(),
1965 self.context.operation,
1966 self.context.phase
1967 )?;
1968 if let Some(violation) = self.validation_violation() {
1969 write!(formatter, " ({violation:?})")?;
1970 }
1971 if self.diagnostics_truncated() {
1972 formatter.write_str(" (diagnostics truncated)")?;
1973 }
1974 if let Some(status) = self.http_status() {
1975 write!(formatter, " (HTTP {status})")?;
1976 }
1977 if let Some(row) = self.context.row {
1978 write!(formatter, " (row {row})")?;
1979 }
1980 if let Some(timeout) = self.timeout() {
1981 write!(formatter, " (deadline {timeout:?})")?;
1982 }
1983 if !self.supplementary().is_empty() {
1984 write!(
1985 formatter,
1986 " ({} supplementary failures)",
1987 self.supplementary().len()
1988 )?;
1989 }
1990 Ok(())
1991 }
1992}
1993
1994impl fmt::Debug for KitError {
1995 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1996 formatter
1997 .debug_struct("KitError")
1998 .field("kind", &self.kind())
1999 .field("basis", &self.classification_basis())
2000 .field("context", &self.context)
2001 .field("validation", &self.validation_violation())
2002 .field("diagnostics_truncated", &self.diagnostics_truncated())
2003 .field("http_status", &self.http_status())
2004 .field("timeout", &self.timeout())
2005 .field("native", &self.native_details())
2006 .field("response", &self.response_details())
2007 .field("cause", &self.cause())
2008 .field("supplementary", &self.supplementary())
2009 .finish()
2010 }
2011}
2012
2013impl std::error::Error for KitError {
2014 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2015 if let Some(cause) = self.cause() {
2016 return Some(cause);
2017 }
2018 self.remote_failure()
2019 .map(|failure| failure as &(dyn std::error::Error + 'static))
2020 }
2021}