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(
1059 /// ErrorKind::NotFound,
1060 /// NativeErrorDetails {
1061 /// message: Some("private detail".into()),
1062 /// ..Default::default()
1063 /// },
1064 /// )?;
1065 /// assert_eq!(failure.kind(), ErrorKind::NotFound);
1066 /// assert_eq!(failure.classification_basis(), ClassificationBasis::Explicit);
1067 /// assert_eq!(failure.native_details().message.as_deref(), Some("private detail"));
1068 /// assert!(!format!("{failure:?}").contains("private detail"));
1069 ///
1070 /// # Ok::<(), providers_kit::KitError>(())
1071 /// ```
1072 ///
1073 pub fn new(kind: ErrorKind, details: NativeErrorDetails) -> Result<Self, KitError> {
1074 let category = RemoteCategory::from_kind(kind)?;
1075 details.validate()?;
1076 Ok(Self {
1077 category,
1078 basis: ClassificationBasis::Explicit,
1079 details: Box::new(details),
1080 })
1081 }
1082
1083 /// Returns the same portable classification vocabulary used by [`KitError`].
1084 ///
1085 /// See [`crate::error::RemoteFailure::new`] for a complete example.
1086 ///
1087 pub fn kind(&self) -> ErrorKind {
1088 self.category.kind()
1089 }
1090
1091 /// Returns the evidence used for classification, independently of the retained body.
1092 ///
1093 /// See [`crate::error::RemoteFailure::new`] for a complete example.
1094 ///
1095 pub fn classification_basis(&self) -> ClassificationBasis {
1096 self.basis
1097 }
1098
1099 /// Explicitly exposes potentially sensitive native evidence.
1100 ///
1101 /// See [`crate::error::RemoteFailure::new`] for a complete example.
1102 ///
1103 pub fn native_details(&self) -> &NativeErrorDetails {
1104 &self.details
1105 }
1106
1107 pub(crate) fn from_native(
1108 kind: ErrorKind,
1109 basis: ClassificationBasis,
1110 mut details: NativeErrorDetails,
1111 ) -> Result<Self, KitError> {
1112 let category = RemoteCategory::from_kind(kind)?;
1113 details.bound();
1114 let failure = Self {
1115 category,
1116 basis,
1117 details: Box::new(details),
1118 };
1119 failure.validate()?;
1120 Ok(failure)
1121 }
1122
1123 fn validate(&self) -> Result<(), KitError> {
1124 self.details.validate()?;
1125 let present = match self.basis {
1126 ClassificationBasis::Explicit => true,
1127 ClassificationBasis::HttpStatus { status } => {
1128 (100..=599).contains(&status)
1129 && !(200..300).contains(&status)
1130 && self.kind() == ErrorKind::from_http_status(status)
1131 }
1132 ClassificationBasis::NativeCode => self
1133 .details
1134 .code
1135 .as_deref()
1136 .is_some_and(|value| !value.trim().is_empty()),
1137 ClassificationBasis::NativeErrorType => self
1138 .details
1139 .error_type
1140 .as_deref()
1141 .is_some_and(|value| !value.trim().is_empty()),
1142 ClassificationBasis::CanonicalStatus => {
1143 self.details.native_status.as_deref().is_some_and(|value| {
1144 let kind = ErrorKind::from_canonical_status(Some(value));
1145 kind != ErrorKind::ProviderRejected && self.kind() == kind
1146 })
1147 }
1148 ClassificationBasis::RpcCode => self.details.rpc_code.is_some_and(|code| {
1149 let kind = ErrorKind::from_rpc_code(Some(code));
1150 kind != ErrorKind::ProviderRejected && self.kind() == kind
1151 }),
1152 ClassificationBasis::NativeReason => self
1153 .details
1154 .details
1155 .iter()
1156 .find_map(|detail| match detail {
1157 NativeDetail::Reason { domain, reason } => {
1158 Some(!domain.trim().is_empty() && !reason.trim().is_empty())
1159 }
1160 _ => None,
1161 })
1162 .unwrap_or(false),
1163 ClassificationBasis::NativeOutcome => match self.details.outcome.as_deref() {
1164 Some("OUTCOME_FAILED" | "URL_RETRIEVAL_STATUS_ERROR") => {
1165 self.kind() == ErrorKind::ProviderRejected
1166 }
1167 Some("OUTCOME_DEADLINE_EXCEEDED") => self.kind() == ErrorKind::DeadlineExceeded,
1168 Some("URL_RETRIEVAL_STATUS_PAYWALL" | "URL_RETRIEVAL_STATUS_UNSAFE") => {
1169 self.kind() == ErrorKind::PermissionDenied
1170 }
1171 None => {
1172 self.details
1173 .execution_exit_code
1174 .is_some_and(|code| code != 0)
1175 && self.kind() == ErrorKind::ProviderRejected
1176 }
1177 Some(_) => false,
1178 },
1179 ClassificationBasis::Unclassified => self.category == RemoteCategory::ProviderRejected,
1180 };
1181 if !present {
1182 return Err(KitError::invalid_input(
1183 ValidationViolation::InvalidValue {
1184 field: "remote_failure.basis",
1185 rule: "requires corresponding classification evidence",
1186 },
1187 "remote classification basis does not have corresponding evidence",
1188 ));
1189 }
1190 Ok(())
1191 }
1192}
1193
1194impl Serialize for RemoteFailure {
1195 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1196 where
1197 S: serde::Serializer,
1198 {
1199 #[derive(Serialize)]
1200 struct Wire<'a> {
1201 kind: RemoteCategory,
1202 basis: ClassificationBasis,
1203 details: &'a NativeErrorDetails,
1204 }
1205 Wire {
1206 kind: self.category,
1207 basis: self.basis,
1208 details: &self.details,
1209 }
1210 .serialize(serializer)
1211 }
1212}
1213
1214impl<'de> Deserialize<'de> for RemoteFailure {
1215 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1216 where
1217 D: serde::Deserializer<'de>,
1218 {
1219 #[derive(Deserialize)]
1220 #[serde(deny_unknown_fields)]
1221 struct Wire {
1222 kind: RemoteCategory,
1223 basis: ClassificationBasis,
1224 details: NativeErrorDetails,
1225 }
1226 let wire = Wire::deserialize(deserializer)?;
1227 let failure = Self {
1228 category: wire.kind,
1229 basis: wire.basis,
1230 details: Box::new(wire.details),
1231 };
1232 failure.validate().map_err(serde::de::Error::custom)?;
1233 Ok(failure)
1234 }
1235}
1236
1237impl fmt::Display for RemoteFailure {
1238 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1239 write!(formatter, "remote {:?} ({:?})", self.kind(), self.basis)
1240 }
1241}
1242
1243impl fmt::Debug for RemoteFailure {
1244 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1245 formatter
1246 .debug_struct("RemoteFailure")
1247 .field("kind", &self.kind())
1248 .field("basis", &self.basis)
1249 .field("details", &self.details)
1250 .finish()
1251 }
1252}
1253
1254impl std::error::Error for RemoteFailure {}
1255
1256#[derive(Clone)]
1257enum ErrorFailure {
1258 Classified {
1259 kind: ErrorKind,
1260 details: Box<NativeErrorDetails>,
1261 },
1262 Remote(RemoteFailure),
1263}
1264
1265impl ErrorFailure {
1266 fn kind(&self) -> ErrorKind {
1267 match self {
1268 Self::Classified { kind, .. } => *kind,
1269 Self::Remote(failure) => failure.kind(),
1270 }
1271 }
1272
1273 fn details(&self) -> &NativeErrorDetails {
1274 match self {
1275 Self::Classified { details, .. } => details,
1276 Self::Remote(failure) => &failure.details,
1277 }
1278 }
1279
1280 fn details_mut(&mut self) -> &mut NativeErrorDetails {
1281 match self {
1282 Self::Classified { details, .. } => details,
1283 Self::Remote(failure) => &mut failure.details,
1284 }
1285 }
1286}
1287
1288#[derive(Clone)]
1289struct ErrorData {
1290 failure: ErrorFailure,
1291 validation: Option<ValidationViolation>,
1292 truncated: bool,
1293 status: Option<u16>,
1294 timeout: Option<Duration>,
1295 response: Option<ResponseDetails>,
1296 request_target: Option<String>,
1297 correlation_id: Option<String>,
1298 cause: Option<Box<KitError>>,
1299 supplementary: Vec<KitError>,
1300}
1301
1302impl ErrorData {
1303 fn bound(&mut self) {
1304 self.bound_tree(&mut DetailBudget::new(), 1);
1305 }
1306
1307 fn bound_tree(&mut self, budget: &mut DetailBudget, depth: usize) {
1308 budget.remaining_nodes -= 1;
1309 let previous_truncation = budget.truncated;
1310 budget.truncated = false;
1311 // Retain this node's observations and local correlation before its native body.
1312 if let Some(response) = self.response.as_mut() {
1313 response.bound(budget);
1314 }
1315 for value in [&mut self.request_target, &mut self.correlation_id]
1316 .into_iter()
1317 .flatten()
1318 {
1319 budget.string(value);
1320 }
1321 self.failure.details_mut().bound_with_budget(budget);
1322 self.truncated |= self.failure.details().truncated;
1323 if depth == ERROR_DEPTH_LIMIT {
1324 self.truncated |= self.cause.is_some() || !self.supplementary.is_empty();
1325 self.cause = None;
1326 self.supplementary.clear();
1327 } else {
1328 if self.cause.as_ref().is_some_and(|cause| {
1329 budget.remaining_nodes == 0 || !cause.data.retains_remote_evidence(budget)
1330 }) {
1331 self.cause = None;
1332 self.truncated = true;
1333 } else if let Some(cause) = self.cause.as_mut() {
1334 cause.data.bound_tree(budget, depth + 1);
1335 }
1336 let mut omitted = false;
1337 self.supplementary.retain_mut(|child| {
1338 if budget.remaining_nodes == 0 || !child.data.retains_remote_evidence(budget) {
1339 omitted = true;
1340 false
1341 } else {
1342 child.data.bound_tree(budget, depth + 1);
1343 true
1344 }
1345 });
1346 self.truncated |= omitted;
1347 }
1348 self.truncated |= budget.truncated;
1349 budget.truncated |= previous_truncation || self.truncated;
1350 }
1351
1352 fn retains_remote_evidence(&self, available: &DetailBudget) -> bool {
1353 let ErrorFailure::Remote(failure) = &self.failure else {
1354 return true;
1355 };
1356 let mut budget = DetailBudget {
1357 remaining: available.remaining,
1358 remaining_nodes: available.remaining_nodes,
1359 truncated: false,
1360 };
1361 if let Some(mut response) = self.response.clone() {
1362 response.bound(&mut budget);
1363 }
1364 for value in [&self.request_target, &self.correlation_id]
1365 .into_iter()
1366 .flatten()
1367 {
1368 budget.string(&mut value.clone());
1369 }
1370 let mut retained = failure.clone();
1371 retained.details.bound_with_budget(&mut budget);
1372 // A child is optional diagnostic context. Omit its subtree instead of
1373 // exposing an invalid serializable RemoteFailure or changing its basis.
1374 retained.validate().is_ok()
1375 }
1376}
1377
1378/// Structured error returned by the portable provider API.
1379///
1380/// ```
1381/// use providers_kit::KitError;
1382/// use providers_kit::error::{ErrorKind, ErrorPhase, Operation, ResponseViolation};
1383/// let error = KitError::new(
1384/// ErrorKind::InvalidResponse(ResponseViolation::MissingField { field: "modelVersion" }),
1385/// Operation::BatchResults,
1386/// ErrorPhase::Normalize,
1387/// ).with_row(0);
1388/// assert_eq!(error.context().row, Some(0));
1389/// assert!(error.to_string().contains("modelVersion"));
1390/// ```
1391///
1392/// Runtime errors are deliberately not serializable. Accepted remote outcomes use
1393/// [`RemoteFailure`]; runtime errors may contain trusted static schema labels and
1394/// causal Kit failures that must not be reconstructed from untrusted JSON.
1395///
1396/// ```compile_fail
1397/// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1398/// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::RequestSend);
1399/// let _ = serde_json::to_string(&error);
1400/// ```
1401#[derive(Clone)]
1402pub struct KitError {
1403 context: ErrorContext,
1404 data: Box<ErrorData>,
1405}
1406
1407impl KitError {
1408 /// Constructs a classified failure without native details.
1409 ///
1410 /// ```
1411 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1412 /// let error = KitError::new(ErrorKind::Unsupported, Operation::Embed, ErrorPhase::InputValidation);
1413 /// assert_eq!(error.kind(), ErrorKind::Unsupported);
1414 /// ```
1415 pub fn new(kind: ErrorKind, operation: Operation, phase: ErrorPhase) -> Self {
1416 Self {
1417 context: ErrorContext {
1418 operation,
1419 phase,
1420 row: None,
1421 },
1422 data: Box::new(ErrorData {
1423 failure: ErrorFailure::Classified {
1424 kind,
1425 details: Box::default(),
1426 },
1427 validation: None,
1428 truncated: false,
1429 status: None,
1430 timeout: None,
1431 response: None,
1432 request_target: None,
1433 correlation_id: None,
1434 cause: None,
1435 supplementary: Vec::new(),
1436 }),
1437 }
1438 }
1439
1440 /// Attaches a remote failure to the operation that observed it.
1441 ///
1442 /// This does not invent an outer HTTP status from an embedded result or stream.
1443 ///
1444 /// # Examples
1445 ///
1446 /// ```
1447 /// use providers_kit::error::{RemoteFailure, NativeErrorDetails, ErrorKind, ClassificationBasis};
1448 /// let failure = RemoteFailure::new(
1449 /// ErrorKind::NotFound,
1450 /// NativeErrorDetails {
1451 /// message: Some("private detail".into()),
1452 /// ..Default::default()
1453 /// },
1454 /// )?;
1455 /// use providers_kit::error::{KitError, Operation, ErrorPhase};
1456 /// let error = KitError::from_remote(failure, Operation::FileInfo, ErrorPhase::ReadBody);
1457 /// assert_eq!(error.kind(), ErrorKind::NotFound);
1458 ///
1459 /// # Ok::<(), providers_kit::KitError>(())
1460 /// ```
1461 ///
1462 pub fn from_remote(failure: RemoteFailure, operation: Operation, phase: ErrorPhase) -> Self {
1463 Self::new(failure.kind(), operation, phase).with_remote_failure(failure)
1464 }
1465
1466 pub(crate) fn with_remote_failure(mut self, failure: RemoteFailure) -> Self {
1467 self.data.failure = ErrorFailure::Remote(failure);
1468 self.data.bound();
1469 self
1470 }
1471
1472 /// Returns the basis for classification; caller construction remains explicit.
1473 ///
1474 /// # Examples
1475 ///
1476 /// ```
1477 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1478 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1479 /// let _basis = error.classification_basis();
1480 /// ```
1481 ///
1482 pub fn classification_basis(&self) -> ClassificationBasis {
1483 match &self.data.failure {
1484 ErrorFailure::Classified { .. } => ClassificationBasis::Explicit,
1485 ErrorFailure::Remote(failure) => failure.classification_basis(),
1486 }
1487 }
1488
1489 /// Returns an attached remote failure, if one was represented explicitly.
1490 ///
1491 /// Absence does not establish that the implementation made no service request.
1492 ///
1493 /// # Examples
1494 ///
1495 /// ```
1496 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1497 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1498 /// assert!(error.remote_failure().is_none());
1499 /// ```
1500 ///
1501 pub fn remote_failure(&self) -> Option<&RemoteFailure> {
1502 match &self.data.failure {
1503 ErrorFailure::Classified { .. } => None,
1504 ErrorFailure::Remote(failure) => Some(failure),
1505 }
1506 }
1507
1508 /// Explicitly exposes potentially sensitive observations for a response violation.
1509 ///
1510 /// # Examples
1511 ///
1512 /// ```
1513 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1514 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1515 /// assert!(error.response_details().is_none());
1516 /// ```
1517 ///
1518 pub fn response_details(&self) -> Option<&ResponseDetails> {
1519 self.data.response.as_ref()
1520 }
1521
1522 /// Returns the safe, detector-assigned local input invariant, when applicable.
1523 ///
1524 /// ```
1525 /// use providers_kit::{generation::GenerationConfig, error::ValidationViolation};
1526 /// let error = GenerationConfig::default().with_temperature(-1.0).unwrap_err();
1527 /// assert!(matches!(error.validation_violation(), Some(ValidationViolation::OutOfRange {
1528 /// field: "generation.temperature", ..
1529 /// })));
1530 /// ```
1531 pub fn validation_violation(&self) -> Option<&ValidationViolation> {
1532 self.data.validation.as_ref()
1533 }
1534
1535 /// Attaches trusted field/rule labels without exposing rejected caller values.
1536 ///
1537 /// ```
1538 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation, ValidationViolation}};
1539 /// let error = KitError::new(ErrorKind::InvalidInput, Operation::ValidateInput, ErrorPhase::InputValidation)
1540 /// .with_validation_violation(ValidationViolation::Empty { field: "messages" });
1541 /// assert_eq!(error.validation_violation(), Some(&ValidationViolation::Empty { field: "messages" }));
1542 /// ```
1543 pub fn with_validation_violation(mut self, violation: ValidationViolation) -> Self {
1544 self.data.validation = Some(violation);
1545 self
1546 }
1547
1548 /// Reports whether this tree lost diagnostics to string, record, depth, or node bounds.
1549 ///
1550 /// A tree retains at most eight levels, 32 nodes, and 1 MiB of diagnostic strings.
1551 /// The primary error is retained before its cause subtree and supplementary errors.
1552 ///
1553 /// ```
1554 /// use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, NativeErrorDetails, Operation}};
1555 /// let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody)
1556 /// .with_native_details(NativeErrorDetails {
1557 /// message: Some("x".repeat(65_537)), ..Default::default()
1558 /// });
1559 /// assert!(error.diagnostics_truncated());
1560 /// ```
1561 pub fn diagnostics_truncated(&self) -> bool {
1562 self.data.truncated
1563 }
1564
1565 /// Returns the cause of this failure, separately from cleanup or diagnostic failures.
1566 ///
1567 /// # Examples
1568 ///
1569 /// ```
1570 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1571 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1572 /// assert!(error.cause().is_none());
1573 /// ```
1574 ///
1575 pub fn cause(&self) -> Option<&Self> {
1576 self.data.cause.as_deref()
1577 }
1578
1579 /// Explicitly exposes the local requested target; this is not native attribution.
1580 ///
1581 /// # Examples
1582 ///
1583 /// ```
1584 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1585 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1586 /// assert!(error.request_target().is_none());
1587 /// ```
1588 ///
1589 pub fn request_target(&self) -> Option<&str> {
1590 self.data.request_target.as_deref()
1591 }
1592
1593 /// Explicitly exposes caller correlation, such as a batch custom ID.
1594 ///
1595 /// # Examples
1596 ///
1597 /// ```
1598 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1599 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1600 /// assert!(error.correlation_id().is_none());
1601 /// ```
1602 ///
1603 pub fn correlation_id(&self) -> Option<&str> {
1604 self.data.correlation_id.as_deref()
1605 }
1606
1607 /// Retains response observations without placing native values in trusted labels.
1608 ///
1609 /// # Examples
1610 ///
1611 /// ```
1612 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1613 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1614 /// let details = providers_kit::error::ResponseDetails::Json {
1615 /// category: providers_kit::error::JsonErrorCategory::Syntax,
1616 /// line: 1,
1617 /// column: 2,
1618 /// };
1619 /// let error = error.with_response_details(details);
1620 /// assert!(error.response_details().is_some());
1621 /// ```
1622 ///
1623 pub fn with_response_details(mut self, details: ResponseDetails) -> Self {
1624 self.data.response = Some(details);
1625 self.data.bound();
1626 self
1627 }
1628
1629 /// Retains an actual causal failure. Independent cleanup belongs in supplementary errors.
1630 ///
1631 /// # Examples
1632 ///
1633 /// ```
1634 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1635 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1636 /// let cause = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1637 /// let error = error.with_cause(cause);
1638 /// assert_eq!(error.cause().unwrap().kind(), ErrorKind::Timeout);
1639 /// ```
1640 ///
1641 pub fn with_cause(mut self, cause: Self) -> Self {
1642 self.data.cause = Some(Box::new(cause));
1643 self.data.bound();
1644 self
1645 }
1646
1647 /// Retains a bounded local target without asserting that the provider identified it.
1648 ///
1649 /// # Examples
1650 ///
1651 /// ```
1652 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1653 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1654 /// let error = error.with_request_target("private-resource");
1655 /// assert_eq!(error.request_target(), Some("private-resource"));
1656 /// assert!(!format!("{error}").contains("private-resource"));
1657 /// ```
1658 ///
1659 pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
1660 self.data.request_target = Some(target.into());
1661 self.data.bound();
1662 self
1663 }
1664
1665 /// Retains bounded caller correlation without treating it as a missing-resource identity.
1666 ///
1667 /// # Examples
1668 ///
1669 /// ```
1670 /// use providers_kit::error::{KitError, ErrorKind, ErrorPhase, Operation};
1671 /// let error = KitError::new(ErrorKind::Closed, Operation::Generate, ErrorPhase::Lifecycle);
1672 /// let error = error.with_correlation_id("row-1");
1673 /// assert_eq!(error.correlation_id(), Some("row-1"));
1674 /// ```
1675 ///
1676 pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
1677 self.data.correlation_id = Some(id.into());
1678 self.data.bound();
1679 self
1680 }
1681
1682 /// Returns the portable classification without interpreting native prose.
1683 ///
1684 /// ```
1685 ///
1686 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1687 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1688 /// assert_eq!(error.kind(), ErrorKind::Transport);
1689 /// ```
1690 pub fn kind(&self) -> ErrorKind {
1691 self.data.failure.kind()
1692 }
1693 /// Returns safe operation, phase and batch row context.
1694 ///
1695 /// ```
1696 ///
1697 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1698 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1699 /// assert_eq!(error.context().operation, Operation::Generate);
1700 /// ```
1701 pub fn context(&self) -> &ErrorContext {
1702 &self.context
1703 }
1704 /// Returns the actual unsuccessful HTTP status, if one was received.
1705 ///
1706 /// ```
1707 ///
1708 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1709 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1710 /// assert_eq!(error.http_status(), None);
1711 /// ```
1712 pub fn http_status(&self) -> Option<u16> {
1713 self.data.status
1714 }
1715 /// Returns the local deadline that expired, if available.
1716 ///
1717 /// ```
1718 ///
1719 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1720 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1721 /// assert_eq!(error.timeout(), None);
1722 /// ```
1723 pub fn timeout(&self) -> Option<Duration> {
1724 self.data.timeout
1725 }
1726 /// Explicitly exposes potentially sensitive native details; do not log them by default.
1727 ///
1728 /// ```
1729 ///
1730 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1731 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1732 /// assert!(error.native_details().message.is_none());
1733 /// ```
1734 pub fn native_details(&self) -> &NativeErrorDetails {
1735 self.data.failure.details()
1736 }
1737 /// Returns failures collecting diagnostics or releasing resources after the primary failure.
1738 ///
1739 /// ```
1740 ///
1741 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1742 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1743 /// assert!(error.supplementary().is_empty());
1744 /// ```
1745 pub fn supplementary(&self) -> &[KitError] {
1746 &self.data.supplementary
1747 }
1748
1749 /// Attaches an observed HTTP status, never a synthetic status inferred from an event code.
1750 ///
1751 /// ```
1752 ///
1753 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1754 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1755 /// let error = error.with_http_status(503);
1756 /// assert_eq!(error.http_status(), Some(503));
1757 /// ```
1758 pub fn with_http_status(mut self, status: u16) -> Self {
1759 self.data.status = Some(status);
1760 self
1761 }
1762 /// Attaches a local deadline; service timeout responses retain their native classification.
1763 ///
1764 /// ```
1765 ///
1766 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1767 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1768 /// let error = error.with_timeout(std::time::Duration::from_secs(10));
1769 /// assert_eq!(error.timeout(), Some(std::time::Duration::from_secs(10)));
1770 /// ```
1771 pub fn with_timeout(mut self, timeout: Duration) -> Self {
1772 self.data.timeout = Some(timeout);
1773 self
1774 }
1775 /// Attaches the zero-based logical batch row, excluding blank JSONL lines.
1776 ///
1777 /// ```
1778 ///
1779 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1780 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1781 /// let error = error.with_row(2);
1782 /// assert_eq!(error.context().row, Some(2));
1783 /// ```
1784 pub fn with_row(mut self, row: usize) -> Self {
1785 self.context.row = Some(row);
1786 self
1787 }
1788 /// Attaches explicit native details, bounding each string to 64 KiB on UTF-8 boundaries.
1789 ///
1790 /// ```
1791 ///
1792 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1793 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1794 /// let details = providers_kit::error::NativeErrorDetails {
1795 /// request_id: Some("sensitive-correlation-id".into()),
1796 /// ..Default::default()
1797 /// };
1798 /// let error = error.with_native_details(details);
1799 /// assert!(!error.to_string().contains("sensitive-correlation-id"));
1800 /// ```
1801 pub fn with_native_details(mut self, mut details: NativeErrorDetails) -> Self {
1802 details.bound();
1803 // Replacing classification evidence is explicit caller construction; it
1804 // must not preserve a stale adapter-derived basis over unrelated details.
1805 match &mut self.data.failure {
1806 ErrorFailure::Classified {
1807 details: current, ..
1808 } => **current = details,
1809 ErrorFailure::Remote(failure) => {
1810 *failure.details = details;
1811 failure.basis = ClassificationBasis::Explicit;
1812 }
1813 }
1814 self.data.bound();
1815 self
1816 }
1817 /// Preserves a secondary failure without replacing this failure's classification or context.
1818 ///
1819 /// ```
1820 ///
1821 /// # use providers_kit::{KitError, error::{ErrorKind, ErrorPhase, Operation}};
1822 /// # let error = KitError::new(ErrorKind::Transport, Operation::Generate, ErrorPhase::ReadBody);
1823 /// let secondary = KitError::new(ErrorKind::Timeout, Operation::Generate, ErrorPhase::ReadBody);
1824 /// let error = error.with_supplementary(secondary);
1825 /// assert_eq!(error.kind(), ErrorKind::Transport);
1826 /// assert_eq!(error.supplementary()[0].kind(), ErrorKind::Timeout);
1827 /// ```
1828 pub fn with_supplementary(mut self, error: Self) -> Self {
1829 self.data.supplementary.push(error);
1830 self.data.bound();
1831 self
1832 }
1833
1834 pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
1835 self.context.operation = operation;
1836 if let Some(cause) = self.data.cause.take() {
1837 self.data.cause = Some(Box::new(
1838 if cause.context.operation == Operation::Unspecified {
1839 cause.at_operation(operation)
1840 } else {
1841 *cause
1842 },
1843 ));
1844 }
1845 self.data.supplementary = self
1846 .data
1847 .supplementary
1848 .into_iter()
1849 .map(|error| {
1850 if error.context.operation == Operation::Unspecified {
1851 error.at_operation(operation)
1852 } else {
1853 error
1854 }
1855 })
1856 .collect();
1857 self
1858 }
1859 pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
1860 self.context.phase = phase;
1861 self
1862 }
1863 pub(crate) fn with_message(mut self, message: impl Into<String>) -> Self {
1864 self.data.failure.details_mut().message = Some(message.into());
1865 self.data.bound();
1866 self
1867 }
1868 pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
1869 self.data.failure.details_mut().body = body;
1870 self.data.bound();
1871 self
1872 }
1873 pub(crate) fn with_diagnostic(mut self, details: Option<NativeErrorDetails>) -> Self {
1874 if let Some(details) = details {
1875 self.data.failure.details_mut().body = details.body;
1876 self.data.failure.details_mut().truncated |= details.truncated;
1877 }
1878 self.data.bound();
1879 self
1880 }
1881 // Adapter-only enrichment preserves the already-validated native basis.
1882 pub(crate) fn with_native_capture(mut self, details: NativeErrorDetails) -> Self {
1883 *self.data.failure.details_mut() = details;
1884 self.data.bound();
1885 self
1886 }
1887 pub(crate) fn config(violation: ValidationViolation, message: impl Into<String>) -> Self {
1888 Self::new(
1889 ErrorKind::Configuration,
1890 Operation::Configure,
1891 ErrorPhase::Configuration,
1892 )
1893 .with_validation_violation(violation)
1894 .with_message(message)
1895 }
1896 pub(crate) fn invalid_input(
1897 violation: ValidationViolation,
1898 message: impl Into<String>,
1899 ) -> Self {
1900 Self::new(
1901 ErrorKind::InvalidInput,
1902 Operation::ValidateInput,
1903 ErrorPhase::InputValidation,
1904 )
1905 .with_validation_violation(violation)
1906 .with_message(message)
1907 }
1908 pub(crate) fn invalid_response(reason: ResponseViolation) -> Self {
1909 Self::new(
1910 ErrorKind::InvalidResponse(reason),
1911 Operation::Unspecified,
1912 ErrorPhase::Normalize,
1913 )
1914 }
1915 pub(crate) fn unsupported(violation: ValidationViolation, message: impl Into<String>) -> Self {
1916 Self::new(
1917 ErrorKind::Unsupported,
1918 Operation::Unspecified,
1919 ErrorPhase::InputValidation,
1920 )
1921 .with_validation_violation(violation)
1922 .with_message(message)
1923 }
1924 pub(crate) fn invalid_state(message: impl Into<String>) -> Self {
1925 Self::new(
1926 ErrorKind::InvalidState,
1927 Operation::Unspecified,
1928 ErrorPhase::Lifecycle,
1929 )
1930 .with_message(message)
1931 }
1932 pub(crate) fn closed(message: impl Into<String>) -> Self {
1933 Self::new(
1934 ErrorKind::Closed,
1935 Operation::Unspecified,
1936 ErrorPhase::Lifecycle,
1937 )
1938 .with_message(message)
1939 }
1940}
1941
1942impl fmt::Display for KitError {
1943 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1944 write!(
1945 formatter,
1946 "{:?} during {:?}/{:?}",
1947 self.kind(),
1948 self.context.operation,
1949 self.context.phase
1950 )?;
1951 if let Some(violation) = self.validation_violation() {
1952 write!(formatter, " ({violation:?})")?;
1953 }
1954 if self.diagnostics_truncated() {
1955 formatter.write_str(" (diagnostics truncated)")?;
1956 }
1957 if let Some(status) = self.http_status() {
1958 write!(formatter, " (HTTP {status})")?;
1959 }
1960 if let Some(row) = self.context.row {
1961 write!(formatter, " (row {row})")?;
1962 }
1963 if let Some(timeout) = self.timeout() {
1964 write!(formatter, " (deadline {timeout:?})")?;
1965 }
1966 if !self.supplementary().is_empty() {
1967 write!(
1968 formatter,
1969 " ({} supplementary failures)",
1970 self.supplementary().len()
1971 )?;
1972 }
1973 Ok(())
1974 }
1975}
1976
1977impl fmt::Debug for KitError {
1978 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1979 formatter
1980 .debug_struct("KitError")
1981 .field("kind", &self.kind())
1982 .field("basis", &self.classification_basis())
1983 .field("context", &self.context)
1984 .field("validation", &self.validation_violation())
1985 .field("diagnostics_truncated", &self.diagnostics_truncated())
1986 .field("http_status", &self.http_status())
1987 .field("timeout", &self.timeout())
1988 .field("native", &self.native_details())
1989 .field("response", &self.response_details())
1990 .field("cause", &self.cause())
1991 .field("supplementary", &self.supplementary())
1992 .finish()
1993 }
1994}
1995
1996impl std::error::Error for KitError {
1997 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1998 if let Some(cause) = self.cause() {
1999 return Some(cause);
2000 }
2001 self.remote_failure()
2002 .map(|failure| failure as &(dyn std::error::Error + 'static))
2003 }
2004}