use std::fmt;
use std::time::Duration;
use serde::{Deserialize, Serialize};
const DETAIL_STRING_LIMIT: usize = 65_536;
const DETAIL_TOTAL_LIMIT: usize = 1_048_576;
const DETAIL_RECORD_LIMIT: usize = 256;
const ERROR_DEPTH_LIMIT: usize = 8;
const ERROR_NODE_LIMIT: usize = 32;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
pub enum ClassificationBasis {
Explicit,
HttpStatus {
status: u16,
},
NativeCode,
NativeErrorType,
CanonicalStatus,
RpcCode,
NativeReason,
NativeOutcome,
Unclassified,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationViolation {
MissingField {
field: &'static str,
},
Empty {
field: &'static str,
},
InvalidFormat {
field: &'static str,
rule: &'static str,
},
InvalidValue {
field: &'static str,
rule: &'static str,
},
OutOfRange {
field: &'static str,
rule: &'static str,
},
Duplicate {
field: &'static str,
},
LimitExceeded {
field: &'static str,
limit: usize,
},
Incompatible {
field: &'static str,
other: &'static str,
},
Unsupported {
field: &'static str,
rule: &'static str,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum JsonErrorCategory {
Io,
Syntax,
Data,
Eof,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum JsonValueKind {
Absent,
Null,
Boolean,
Number,
String,
Array,
Object,
}
impl JsonValueKind {
pub(crate) fn of(value: Option<&serde_json::Value>) -> Self {
match value {
None => Self::Absent,
Some(serde_json::Value::Null) => Self::Null,
Some(serde_json::Value::Bool(_)) => Self::Boolean,
Some(serde_json::Value::Number(_)) => Self::Number,
Some(serde_json::Value::String(_)) => Self::String,
Some(serde_json::Value::Array(_)) => Self::Array,
Some(serde_json::Value::Object(_)) => Self::Object,
}
}
}
#[derive(Clone, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseDetails {
Json {
category: JsonErrorCategory,
line: usize,
column: usize,
},
FieldType {
expected: JsonValueKind,
actual: JsonValueKind,
},
Discriminator {
actual: String,
},
Identity {
expected: String,
actual: String,
},
Index {
index: usize,
available: usize,
},
Size {
observed: usize,
},
}
impl ResponseDetails {
pub(crate) fn json(error: &serde_json::Error) -> Self {
Self::Json {
category: match error.classify() {
serde_json::error::Category::Io => JsonErrorCategory::Io,
serde_json::error::Category::Syntax => JsonErrorCategory::Syntax,
serde_json::error::Category::Data => JsonErrorCategory::Data,
serde_json::error::Category::Eof => JsonErrorCategory::Eof,
},
line: error.line(),
column: error.column(),
}
}
fn bound(&mut self, budget: &mut DetailBudget) {
match self {
Self::Discriminator { actual } => budget.string(actual),
Self::Identity { expected, actual } => {
budget.string(expected);
budget.string(actual);
}
Self::Json { .. } | Self::FieldType { .. } | Self::Index { .. } | Self::Size { .. } => {
}
}
}
}
impl fmt::Debug for ResponseDetails {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Json {
category,
line,
column,
} => formatter
.debug_struct("Json")
.field("category", category)
.field("line", line)
.field("column", column)
.finish(),
Self::FieldType { expected, actual } => formatter
.debug_struct("FieldType")
.field("expected", expected)
.field("actual", actual)
.finish(),
Self::Index { index, available } => formatter
.debug_struct("Index")
.field("index", index)
.field("available", available)
.finish(),
Self::Size { observed } => formatter
.debug_struct("Size")
.field("observed", observed)
.finish(),
Self::Discriminator { .. } => {
formatter.write_str("Discriminator { actual: <redacted> }")
}
Self::Identity { .. } => {
formatter.write_str("Identity { expected: <redacted>, actual: <redacted> }")
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
Configuration,
InvalidInput,
Unsupported,
Transport,
Timeout,
DeadlineExceeded,
Closed,
InvalidState,
InvalidResponse(ResponseViolation),
InvalidRequest,
Authentication,
PermissionDenied,
NotFound,
Conflict,
RequestTooLarge,
FailedPrecondition,
ResourceExhausted,
RateLimited,
QuotaExceeded,
Billing,
ServiceUnavailable,
Internal,
Cancelled,
ProviderRejected,
}
impl ErrorKind {
pub(crate) fn from_http_status(status: u16) -> Self {
match status {
400 | 422 => Self::InvalidRequest,
401 => Self::Authentication,
402 => Self::Billing,
403 => Self::PermissionDenied,
404 => Self::NotFound,
408 | 504 => Self::DeadlineExceeded,
409 => Self::Conflict,
412 => Self::FailedPrecondition,
413 => Self::RequestTooLarge,
429 => Self::ResourceExhausted,
500 => Self::Internal,
502 | 503 | 529 => Self::ServiceUnavailable,
_ => Self::ProviderRejected,
}
}
pub(crate) fn from_canonical_status(status: Option<&str>) -> ErrorKind {
match status {
Some("INVALID_ARGUMENT" | "OUT_OF_RANGE") => Self::InvalidRequest,
Some("UNAUTHENTICATED") => Self::Authentication,
Some("PERMISSION_DENIED") => Self::PermissionDenied,
Some("NOT_FOUND") => Self::NotFound,
Some("ALREADY_EXISTS" | "ABORTED") => Self::Conflict,
Some("RESOURCE_EXHAUSTED") => Self::ResourceExhausted,
Some("FAILED_PRECONDITION") => Self::FailedPrecondition,
Some("DEADLINE_EXCEEDED") => Self::DeadlineExceeded,
Some("CANCELLED") => Self::Cancelled,
Some("UNAVAILABLE") => Self::ServiceUnavailable,
Some("INTERNAL" | "DATA_LOSS" | "UNKNOWN") => Self::Internal,
Some("UNIMPLEMENTED") => Self::Unsupported,
_ => Self::ProviderRejected,
}
}
pub(crate) fn from_rpc_code(code: Option<i64>) -> ErrorKind {
match code {
Some(1) => Self::Cancelled,
Some(2 | 13 | 15) => Self::Internal,
Some(3 | 11) => Self::InvalidRequest,
Some(4) => Self::DeadlineExceeded,
Some(5) => Self::NotFound,
Some(6 | 10) => Self::Conflict,
Some(7) => Self::PermissionDenied,
Some(8) => Self::ResourceExhausted,
Some(9) => Self::FailedPrecondition,
Some(12) => Self::Unsupported,
Some(14) => Self::ServiceUnavailable,
Some(16) => Self::Authentication,
_ => Self::ProviderRejected,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResponseViolation {
MalformedJson,
InvalidUtf8,
MissingField {
field: &'static str,
},
InvalidField {
field: &'static str,
},
UnknownVariant {
field: &'static str,
},
IdentityMismatch {
field: &'static str,
},
ProtocolViolation {
rule: &'static str,
},
UnexpectedEnd,
LimitExceeded {
limit: usize,
},
}
impl ResponseViolation {
pub(crate) fn from_json_error(error: &serde_json::Error, field: &'static str) -> Self {
match error.classify() {
serde_json::error::Category::Data => Self::InvalidField { field },
serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
Self::MalformedJson
}
serde_json::error::Category::Io => Self::ProtocolViolation {
rule: "JSON input read failure",
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Operation {
Unspecified,
Configure,
ValidateInput,
SelectModel,
ListModels,
ModelInfo,
UploadFile,
ListFiles,
FileInfo,
DeleteFile,
DownloadFile,
BatchStatus,
CancelBatch,
BatchResults,
ListBatches,
Health,
Shutdown,
Generate,
GenerateStream,
Embed,
SubmitBatch,
OpenSession,
SessionSend,
SessionReceive,
SessionClose,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorPhase {
Unspecified,
Configuration,
InputValidation,
RequestBuild,
RequestSend,
ResponseHeaders,
ReadBody,
Decode,
Normalize,
Stream,
SessionHandshake,
SessionSend,
SessionReceive,
SessionClose,
Lifecycle,
Cleanup,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ErrorContext {
pub operation: Operation,
pub phase: ErrorPhase,
pub row: Option<usize>,
}
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum NativeDetail {
Reason {
domain: String,
reason: String,
},
Resource {
resource_type: Option<String>,
name: String,
owner: Option<String>,
description: Option<String>,
},
FieldViolation {
field: String,
reason: Option<String>,
description: Option<String>,
},
RetryDelay {
seconds: u64,
nanos: u32,
},
QuotaViolation {
subject: Option<String>,
description: Option<String>,
},
PreconditionViolation {
kind: Option<String>,
subject: Option<String>,
description: Option<String>,
},
}
impl NativeDetail {
fn strings(&self) -> [Option<&str>; 4] {
match self {
Self::Reason { domain, reason } => [Some(domain), Some(reason), None, None],
Self::Resource {
resource_type,
name,
owner,
description,
} => [
resource_type.as_deref(),
Some(name),
owner.as_deref(),
description.as_deref(),
],
Self::FieldViolation {
field,
reason,
description,
} => [Some(field), reason.as_deref(), description.as_deref(), None],
Self::QuotaViolation {
subject,
description,
} => [subject.as_deref(), description.as_deref(), None, None],
Self::PreconditionViolation {
kind,
subject,
description,
} => [
kind.as_deref(),
subject.as_deref(),
description.as_deref(),
None,
],
Self::RetryDelay { .. } => [None, None, None, None],
}
}
fn bound(&mut self, budget: &mut DetailBudget) {
match self {
Self::Reason { domain, reason } => {
budget.string(domain);
budget.string(reason);
}
Self::Resource {
resource_type,
name,
owner,
description,
} => {
budget.string(name);
for value in [resource_type, owner, description].into_iter().flatten() {
budget.string(value);
}
}
Self::FieldViolation {
field,
reason,
description,
} => {
budget.string(field);
for value in [reason, description].into_iter().flatten() {
budget.string(value);
}
}
Self::QuotaViolation {
subject,
description,
} => {
for value in [subject, description].into_iter().flatten() {
budget.string(value);
}
}
Self::PreconditionViolation {
kind,
subject,
description,
} => {
for value in [kind, subject, description].into_iter().flatten() {
budget.string(value);
}
}
Self::RetryDelay { .. } => {}
}
}
}
impl fmt::Debug for NativeDetail {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Reason { .. } => "Reason",
Self::Resource { .. } => "Resource",
Self::FieldViolation { .. } => "FieldViolation",
Self::RetryDelay { .. } => "RetryDelay",
Self::QuotaViolation { .. } => "QuotaViolation",
Self::PreconditionViolation { .. } => "PreconditionViolation",
};
formatter.write_str(name)?;
formatter.write_str(" { details: <redacted> }")
}
}
struct DetailBudget {
remaining: usize,
remaining_nodes: usize,
truncated: bool,
}
impl DetailBudget {
fn new() -> Self {
Self {
remaining: DETAIL_TOTAL_LIMIT,
remaining_nodes: ERROR_NODE_LIMIT,
truncated: false,
}
}
fn string(&mut self, value: &mut String) {
let limit = DETAIL_STRING_LIMIT.min(self.remaining);
if value.len() > limit {
let mut end = limit;
while !value.is_char_boundary(end) {
end -= 1;
}
value.truncate(end);
self.truncated = true;
}
self.remaining -= value.len();
}
}
#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct NativeErrorDetails {
pub message: Option<String>,
pub body: Option<String>,
pub code: Option<String>,
pub error_type: Option<String>,
pub event_type: Option<String>,
pub outcome: Option<String>,
pub parameter: Option<String>,
pub request_id: Option<String>,
pub event_id: Option<String>,
pub related_event_id: Option<String>,
pub tool_call_id: Option<String>,
pub sequence_number: Option<u64>,
pub native_status: Option<String>,
pub reported_http_status: Option<u16>,
pub rpc_code: Option<i64>,
pub execution_exit_code: Option<i64>,
pub retry_after: Option<String>,
pub url: Option<String>,
pub details: Vec<NativeDetail>,
pub truncated: bool,
pub omitted_details: usize,
}
impl NativeErrorDetails {
fn bound(&mut self) {
let mut budget = DetailBudget::new();
self.bound_with_budget(&mut budget);
}
fn bound_with_budget(&mut self, budget: &mut DetailBudget) {
for value in [
&mut self.code,
&mut self.error_type,
&mut self.native_status,
&mut self.outcome,
]
.into_iter()
.flatten()
{
budget.string(value);
}
let reason_index = self
.details
.iter()
.position(|detail| matches!(detail, NativeDetail::Reason { .. }));
if let Some(index) = reason_index {
self.details[index].bound(budget);
if index >= DETAIL_RECORD_LIMIT {
let reason = self.details.remove(index);
self.details.insert(0, reason);
}
}
let omitted = self.details.len().saturating_sub(DETAIL_RECORD_LIMIT);
self.omitted_details = self.omitted_details.saturating_add(omitted);
self.details.truncate(DETAIL_RECORD_LIMIT);
let retained_reason = self
.details
.iter()
.position(|detail| matches!(detail, NativeDetail::Reason { .. }));
for (index, detail) in self.details.iter_mut().enumerate() {
if Some(index) != retained_reason {
detail.bound(budget);
}
}
for value in [
&mut self.event_type,
&mut self.parameter,
&mut self.request_id,
&mut self.event_id,
&mut self.related_event_id,
&mut self.tool_call_id,
&mut self.retry_after,
&mut self.url,
&mut self.message,
&mut self.body,
]
.into_iter()
.flatten()
{
budget.string(value);
}
self.truncated |= budget.truncated || self.omitted_details != 0;
}
fn validate(&self) -> Result<(), KitError> {
if self.details.len() > DETAIL_RECORD_LIMIT {
return Err(KitError::invalid_input(
ValidationViolation::LimitExceeded {
field: "native_details.details",
limit: DETAIL_RECORD_LIMIT,
},
"native details exceed 256 records",
));
}
if self.omitted_details != 0 && !self.truncated {
return Err(KitError::invalid_input(
ValidationViolation::InvalidValue {
field: "native_details.truncated",
rule: "required when detail records were omitted",
},
"omitted native details require a truncation marker",
));
}
let named = [
self.message.as_deref(),
self.body.as_deref(),
self.code.as_deref(),
self.error_type.as_deref(),
self.event_type.as_deref(),
self.outcome.as_deref(),
self.parameter.as_deref(),
self.request_id.as_deref(),
self.event_id.as_deref(),
self.related_event_id.as_deref(),
self.tool_call_id.as_deref(),
self.native_status.as_deref(),
self.retry_after.as_deref(),
self.url.as_deref(),
];
let strings = named.into_iter().flatten().chain(
self.details
.iter()
.flat_map(|detail| detail.strings().into_iter().flatten()),
);
let mut total = 0_usize;
for value in strings {
if value.len() > DETAIL_STRING_LIMIT {
return Err(KitError::invalid_input(
ValidationViolation::LimitExceeded {
field: "native_details.string",
limit: DETAIL_STRING_LIMIT,
},
"native detail exceeds 64 KiB",
));
}
total = total.saturating_add(value.len());
}
if total > DETAIL_TOTAL_LIMIT {
return Err(KitError::invalid_input(
ValidationViolation::LimitExceeded {
field: "native_details",
limit: DETAIL_TOTAL_LIMIT,
},
"native details exceed 1 MiB in total",
));
}
Ok(())
}
}
impl fmt::Debug for NativeErrorDetails {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("NativeErrorDetails")
.field("details", &"<redacted>")
.field("truncated", &self.truncated)
.field("omitted_details", &self.omitted_details)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RemoteCategory {
Unsupported,
InvalidRequest,
Authentication,
PermissionDenied,
NotFound,
Conflict,
RequestTooLarge,
FailedPrecondition,
ResourceExhausted,
RateLimited,
QuotaExceeded,
Billing,
ServiceUnavailable,
Internal,
DeadlineExceeded,
Cancelled,
ProviderRejected,
}
impl RemoteCategory {
fn from_kind(kind: ErrorKind) -> Result<Self, KitError> {
match kind {
ErrorKind::Unsupported => Ok(Self::Unsupported),
ErrorKind::InvalidRequest => Ok(Self::InvalidRequest),
ErrorKind::Authentication => Ok(Self::Authentication),
ErrorKind::PermissionDenied => Ok(Self::PermissionDenied),
ErrorKind::NotFound => Ok(Self::NotFound),
ErrorKind::Conflict => Ok(Self::Conflict),
ErrorKind::RequestTooLarge => Ok(Self::RequestTooLarge),
ErrorKind::FailedPrecondition => Ok(Self::FailedPrecondition),
ErrorKind::ResourceExhausted => Ok(Self::ResourceExhausted),
ErrorKind::RateLimited => Ok(Self::RateLimited),
ErrorKind::QuotaExceeded => Ok(Self::QuotaExceeded),
ErrorKind::Billing => Ok(Self::Billing),
ErrorKind::ServiceUnavailable => Ok(Self::ServiceUnavailable),
ErrorKind::Internal => Ok(Self::Internal),
ErrorKind::DeadlineExceeded => Ok(Self::DeadlineExceeded),
ErrorKind::Cancelled => Ok(Self::Cancelled),
ErrorKind::ProviderRejected => Ok(Self::ProviderRejected),
ErrorKind::Configuration
| ErrorKind::InvalidInput
| ErrorKind::Transport
| ErrorKind::Timeout
| ErrorKind::Closed
| ErrorKind::InvalidState
| ErrorKind::InvalidResponse(_) => Err(KitError::invalid_input(
ValidationViolation::InvalidValue {
field: "remote_failure.kind",
rule: "must classify a remote outcome",
},
"remote failure cannot use a local or response-schema classification",
)),
}
}
fn kind(self) -> ErrorKind {
match self {
Self::Unsupported => ErrorKind::Unsupported,
Self::InvalidRequest => ErrorKind::InvalidRequest,
Self::Authentication => ErrorKind::Authentication,
Self::PermissionDenied => ErrorKind::PermissionDenied,
Self::NotFound => ErrorKind::NotFound,
Self::Conflict => ErrorKind::Conflict,
Self::RequestTooLarge => ErrorKind::RequestTooLarge,
Self::FailedPrecondition => ErrorKind::FailedPrecondition,
Self::ResourceExhausted => ErrorKind::ResourceExhausted,
Self::RateLimited => ErrorKind::RateLimited,
Self::QuotaExceeded => ErrorKind::QuotaExceeded,
Self::Billing => ErrorKind::Billing,
Self::ServiceUnavailable => ErrorKind::ServiceUnavailable,
Self::Internal => ErrorKind::Internal,
Self::DeadlineExceeded => ErrorKind::DeadlineExceeded,
Self::Cancelled => ErrorKind::Cancelled,
Self::ProviderRejected => ErrorKind::ProviderRejected,
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct RemoteFailure {
category: RemoteCategory,
basis: ClassificationBasis,
details: Box<NativeErrorDetails>,
}
impl RemoteFailure {
pub fn new(kind: ErrorKind, details: NativeErrorDetails) -> Result<Self, KitError> {
let category = RemoteCategory::from_kind(kind)?;
details.validate()?;
Ok(Self {
category,
basis: ClassificationBasis::Explicit,
details: Box::new(details),
})
}
pub fn kind(&self) -> ErrorKind {
self.category.kind()
}
pub fn classification_basis(&self) -> ClassificationBasis {
self.basis
}
pub fn native_details(&self) -> &NativeErrorDetails {
&self.details
}
pub(crate) fn from_native(
kind: ErrorKind,
basis: ClassificationBasis,
mut details: NativeErrorDetails,
) -> Result<Self, KitError> {
let category = RemoteCategory::from_kind(kind)?;
details.bound();
let failure = Self {
category,
basis,
details: Box::new(details),
};
failure.validate()?;
Ok(failure)
}
fn validate(&self) -> Result<(), KitError> {
self.details.validate()?;
let present = match self.basis {
ClassificationBasis::Explicit => true,
ClassificationBasis::HttpStatus { status } => {
(100..=599).contains(&status)
&& !(200..300).contains(&status)
&& self.kind() == ErrorKind::from_http_status(status)
}
ClassificationBasis::NativeCode => self
.details
.code
.as_deref()
.is_some_and(|value| !value.trim().is_empty()),
ClassificationBasis::NativeErrorType => self
.details
.error_type
.as_deref()
.is_some_and(|value| !value.trim().is_empty()),
ClassificationBasis::CanonicalStatus => {
self.details.native_status.as_deref().is_some_and(|value| {
let kind = ErrorKind::from_canonical_status(Some(value));
kind != ErrorKind::ProviderRejected && self.kind() == kind
})
}
ClassificationBasis::RpcCode => self.details.rpc_code.is_some_and(|code| {
let kind = ErrorKind::from_rpc_code(Some(code));
kind != ErrorKind::ProviderRejected && self.kind() == kind
}),
ClassificationBasis::NativeReason => self
.details
.details
.iter()
.find_map(|detail| match detail {
NativeDetail::Reason { domain, reason } => {
Some(!domain.trim().is_empty() && !reason.trim().is_empty())
}
_ => None,
})
.unwrap_or(false),
ClassificationBasis::NativeOutcome => match self.details.outcome.as_deref() {
Some("OUTCOME_FAILED" | "URL_RETRIEVAL_STATUS_ERROR") => {
self.kind() == ErrorKind::ProviderRejected
}
Some("OUTCOME_DEADLINE_EXCEEDED") => self.kind() == ErrorKind::DeadlineExceeded,
Some("URL_RETRIEVAL_STATUS_PAYWALL" | "URL_RETRIEVAL_STATUS_UNSAFE") => {
self.kind() == ErrorKind::PermissionDenied
}
None => {
self.details
.execution_exit_code
.is_some_and(|code| code != 0)
&& self.kind() == ErrorKind::ProviderRejected
}
Some(_) => false,
},
ClassificationBasis::Unclassified => self.category == RemoteCategory::ProviderRejected,
};
if !present {
return Err(KitError::invalid_input(
ValidationViolation::InvalidValue {
field: "remote_failure.basis",
rule: "requires corresponding classification evidence",
},
"remote classification basis does not have corresponding evidence",
));
}
Ok(())
}
}
impl Serialize for RemoteFailure {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct Wire<'a> {
kind: RemoteCategory,
basis: ClassificationBasis,
details: &'a NativeErrorDetails,
}
Wire {
kind: self.category,
basis: self.basis,
details: &self.details,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for RemoteFailure {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
kind: RemoteCategory,
basis: ClassificationBasis,
details: NativeErrorDetails,
}
let wire = Wire::deserialize(deserializer)?;
let failure = Self {
category: wire.kind,
basis: wire.basis,
details: Box::new(wire.details),
};
failure.validate().map_err(serde::de::Error::custom)?;
Ok(failure)
}
}
impl fmt::Display for RemoteFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "remote {:?} ({:?})", self.kind(), self.basis)
}
}
impl fmt::Debug for RemoteFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RemoteFailure")
.field("kind", &self.kind())
.field("basis", &self.basis)
.field("details", &self.details)
.finish()
}
}
impl std::error::Error for RemoteFailure {}
#[derive(Clone)]
enum ErrorFailure {
Classified {
kind: ErrorKind,
details: Box<NativeErrorDetails>,
},
Remote(RemoteFailure),
}
impl ErrorFailure {
fn kind(&self) -> ErrorKind {
match self {
Self::Classified { kind, .. } => *kind,
Self::Remote(failure) => failure.kind(),
}
}
fn details(&self) -> &NativeErrorDetails {
match self {
Self::Classified { details, .. } => details,
Self::Remote(failure) => &failure.details,
}
}
fn details_mut(&mut self) -> &mut NativeErrorDetails {
match self {
Self::Classified { details, .. } => details,
Self::Remote(failure) => &mut failure.details,
}
}
}
#[derive(Clone)]
struct ErrorData {
failure: ErrorFailure,
validation: Option<ValidationViolation>,
truncated: bool,
status: Option<u16>,
timeout: Option<Duration>,
response: Option<ResponseDetails>,
request_target: Option<String>,
correlation_id: Option<String>,
cause: Option<Box<KitError>>,
supplementary: Vec<KitError>,
}
impl ErrorData {
fn bound(&mut self) {
self.bound_tree(&mut DetailBudget::new(), 1);
}
fn bound_tree(&mut self, budget: &mut DetailBudget, depth: usize) {
budget.remaining_nodes -= 1;
let previous_truncation = budget.truncated;
budget.truncated = false;
if let Some(response) = self.response.as_mut() {
response.bound(budget);
}
for value in [&mut self.request_target, &mut self.correlation_id]
.into_iter()
.flatten()
{
budget.string(value);
}
self.failure.details_mut().bound_with_budget(budget);
self.truncated |= self.failure.details().truncated;
if depth == ERROR_DEPTH_LIMIT {
self.truncated |= self.cause.is_some() || !self.supplementary.is_empty();
self.cause = None;
self.supplementary.clear();
} else {
if self.cause.as_ref().is_some_and(|cause| {
budget.remaining_nodes == 0 || !cause.data.retains_remote_evidence(budget)
}) {
self.cause = None;
self.truncated = true;
} else if let Some(cause) = self.cause.as_mut() {
cause.data.bound_tree(budget, depth + 1);
}
let mut omitted = false;
self.supplementary.retain_mut(|child| {
if budget.remaining_nodes == 0 || !child.data.retains_remote_evidence(budget) {
omitted = true;
false
} else {
child.data.bound_tree(budget, depth + 1);
true
}
});
self.truncated |= omitted;
}
self.truncated |= budget.truncated;
budget.truncated |= previous_truncation || self.truncated;
}
fn retains_remote_evidence(&self, available: &DetailBudget) -> bool {
let ErrorFailure::Remote(failure) = &self.failure else {
return true;
};
let mut budget = DetailBudget {
remaining: available.remaining,
remaining_nodes: available.remaining_nodes,
truncated: false,
};
if let Some(mut response) = self.response.clone() {
response.bound(&mut budget);
}
for value in [&self.request_target, &self.correlation_id]
.into_iter()
.flatten()
{
budget.string(&mut value.clone());
}
let mut retained = failure.clone();
retained.details.bound_with_budget(&mut budget);
retained.validate().is_ok()
}
}
#[derive(Clone)]
pub struct KitError {
context: ErrorContext,
data: Box<ErrorData>,
}
impl KitError {
pub fn new(kind: ErrorKind, operation: Operation, phase: ErrorPhase) -> Self {
Self {
context: ErrorContext {
operation,
phase,
row: None,
},
data: Box::new(ErrorData {
failure: ErrorFailure::Classified {
kind,
details: Box::default(),
},
validation: None,
truncated: false,
status: None,
timeout: None,
response: None,
request_target: None,
correlation_id: None,
cause: None,
supplementary: Vec::new(),
}),
}
}
pub fn from_remote(failure: RemoteFailure, operation: Operation, phase: ErrorPhase) -> Self {
Self::new(failure.kind(), operation, phase).with_remote_failure(failure)
}
pub(crate) fn with_remote_failure(mut self, failure: RemoteFailure) -> Self {
self.data.failure = ErrorFailure::Remote(failure);
self.data.bound();
self
}
pub fn classification_basis(&self) -> ClassificationBasis {
match &self.data.failure {
ErrorFailure::Classified { .. } => ClassificationBasis::Explicit,
ErrorFailure::Remote(failure) => failure.classification_basis(),
}
}
pub fn remote_failure(&self) -> Option<&RemoteFailure> {
match &self.data.failure {
ErrorFailure::Classified { .. } => None,
ErrorFailure::Remote(failure) => Some(failure),
}
}
pub fn response_details(&self) -> Option<&ResponseDetails> {
self.data.response.as_ref()
}
pub fn validation_violation(&self) -> Option<&ValidationViolation> {
self.data.validation.as_ref()
}
pub fn with_validation_violation(mut self, violation: ValidationViolation) -> Self {
self.data.validation = Some(violation);
self
}
pub fn diagnostics_truncated(&self) -> bool {
self.data.truncated
}
pub fn cause(&self) -> Option<&Self> {
self.data.cause.as_deref()
}
pub fn request_target(&self) -> Option<&str> {
self.data.request_target.as_deref()
}
pub fn correlation_id(&self) -> Option<&str> {
self.data.correlation_id.as_deref()
}
pub fn with_response_details(mut self, details: ResponseDetails) -> Self {
self.data.response = Some(details);
self.data.bound();
self
}
pub fn with_cause(mut self, cause: Self) -> Self {
self.data.cause = Some(Box::new(cause));
self.data.bound();
self
}
pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
self.data.request_target = Some(target.into());
self.data.bound();
self
}
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
self.data.correlation_id = Some(id.into());
self.data.bound();
self
}
pub fn kind(&self) -> ErrorKind {
self.data.failure.kind()
}
pub fn context(&self) -> &ErrorContext {
&self.context
}
pub fn http_status(&self) -> Option<u16> {
self.data.status
}
pub fn timeout(&self) -> Option<Duration> {
self.data.timeout
}
pub fn native_details(&self) -> &NativeErrorDetails {
self.data.failure.details()
}
pub fn supplementary(&self) -> &[KitError] {
&self.data.supplementary
}
pub fn with_http_status(mut self, status: u16) -> Self {
self.data.status = Some(status);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.data.timeout = Some(timeout);
self
}
pub fn with_row(mut self, row: usize) -> Self {
self.context.row = Some(row);
self
}
pub fn with_native_details(mut self, mut details: NativeErrorDetails) -> Self {
details.bound();
match &mut self.data.failure {
ErrorFailure::Classified {
details: current, ..
} => **current = details,
ErrorFailure::Remote(failure) => {
*failure.details = details;
failure.basis = ClassificationBasis::Explicit;
}
}
self.data.bound();
self
}
pub fn with_supplementary(mut self, error: Self) -> Self {
self.data.supplementary.push(error);
self.data.bound();
self
}
pub(crate) fn at_operation(mut self, operation: Operation) -> Self {
self.context.operation = operation;
if let Some(cause) = self.data.cause.take() {
self.data.cause = Some(Box::new(
if cause.context.operation == Operation::Unspecified {
cause.at_operation(operation)
} else {
*cause
},
));
}
self.data.supplementary = self
.data
.supplementary
.into_iter()
.map(|error| {
if error.context.operation == Operation::Unspecified {
error.at_operation(operation)
} else {
error
}
})
.collect();
self
}
pub(crate) fn at_phase(mut self, phase: ErrorPhase) -> Self {
self.context.phase = phase;
self
}
pub(crate) fn with_message(mut self, message: impl Into<String>) -> Self {
self.data.failure.details_mut().message = Some(message.into());
self.data.bound();
self
}
pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
self.data.failure.details_mut().body = body;
self.data.bound();
self
}
pub(crate) fn with_diagnostic(mut self, details: Option<NativeErrorDetails>) -> Self {
if let Some(details) = details {
self.data.failure.details_mut().body = details.body;
self.data.failure.details_mut().truncated |= details.truncated;
}
self.data.bound();
self
}
pub(crate) fn with_native_capture(mut self, details: NativeErrorDetails) -> Self {
*self.data.failure.details_mut() = details;
self.data.bound();
self
}
pub(crate) fn config(violation: ValidationViolation, message: impl Into<String>) -> Self {
Self::new(
ErrorKind::Configuration,
Operation::Configure,
ErrorPhase::Configuration,
)
.with_validation_violation(violation)
.with_message(message)
}
pub(crate) fn invalid_input(
violation: ValidationViolation,
message: impl Into<String>,
) -> Self {
Self::new(
ErrorKind::InvalidInput,
Operation::ValidateInput,
ErrorPhase::InputValidation,
)
.with_validation_violation(violation)
.with_message(message)
}
pub(crate) fn invalid_response(reason: ResponseViolation) -> Self {
Self::new(
ErrorKind::InvalidResponse(reason),
Operation::Unspecified,
ErrorPhase::Normalize,
)
}
pub(crate) fn unsupported(violation: ValidationViolation, message: impl Into<String>) -> Self {
Self::new(
ErrorKind::Unsupported,
Operation::Unspecified,
ErrorPhase::InputValidation,
)
.with_validation_violation(violation)
.with_message(message)
}
pub(crate) fn invalid_state(message: impl Into<String>) -> Self {
Self::new(
ErrorKind::InvalidState,
Operation::Unspecified,
ErrorPhase::Lifecycle,
)
.with_message(message)
}
pub(crate) fn closed(message: impl Into<String>) -> Self {
Self::new(
ErrorKind::Closed,
Operation::Unspecified,
ErrorPhase::Lifecycle,
)
.with_message(message)
}
}
impl fmt::Display for KitError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{:?} during {:?}/{:?}",
self.kind(),
self.context.operation,
self.context.phase
)?;
if let Some(violation) = self.validation_violation() {
write!(formatter, " ({violation:?})")?;
}
if self.diagnostics_truncated() {
formatter.write_str(" (diagnostics truncated)")?;
}
if let Some(status) = self.http_status() {
write!(formatter, " (HTTP {status})")?;
}
if let Some(row) = self.context.row {
write!(formatter, " (row {row})")?;
}
if let Some(timeout) = self.timeout() {
write!(formatter, " (deadline {timeout:?})")?;
}
if !self.supplementary().is_empty() {
write!(
formatter,
" ({} supplementary failures)",
self.supplementary().len()
)?;
}
Ok(())
}
}
impl fmt::Debug for KitError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("KitError")
.field("kind", &self.kind())
.field("basis", &self.classification_basis())
.field("context", &self.context)
.field("validation", &self.validation_violation())
.field("diagnostics_truncated", &self.diagnostics_truncated())
.field("http_status", &self.http_status())
.field("timeout", &self.timeout())
.field("native", &self.native_details())
.field("response", &self.response_details())
.field("cause", &self.cause())
.field("supplementary", &self.supplementary())
.finish()
}
}
impl std::error::Error for KitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Some(cause) = self.cause() {
return Some(cause);
}
self.remote_failure()
.map(|failure| failure as &(dyn std::error::Error + 'static))
}
}