use crate::error_code::StandardErrorCode;
#[cfg(not(target_arch = "wasm32"))]
use crate::pipeline::PipelineError;
use crate::protocol::ProtocolError;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorContext {
pub field_path: Option<String>,
pub details: Option<String>,
pub source: Option<String>,
pub hint: Option<String>,
pub request_id: Option<String>,
pub status_code: Option<u16>,
pub error_code: Option<String>,
pub retryable: Option<bool>,
pub fallbackable: Option<bool>,
pub standard_code: Option<StandardErrorCode>,
}
impl ErrorContext {
pub fn new() -> Self {
Self {
field_path: None,
details: None,
source: None,
hint: None,
request_id: None,
status_code: None,
error_code: None,
retryable: None,
fallbackable: None,
standard_code: None,
}
}
pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
self.field_path = Some(path.into());
self
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = Some(id.into());
self
}
pub fn with_status_code(mut self, code: u16) -> Self {
self.status_code = Some(code);
self
}
pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
self.error_code = Some(code.into());
self
}
pub fn with_retryable(mut self, retryable: bool) -> Self {
self.retryable = Some(retryable);
self
}
pub fn with_fallbackable(mut self, fallbackable: bool) -> Self {
self.fallbackable = Some(fallbackable);
self
}
pub fn with_standard_code(mut self, code: StandardErrorCode) -> Self {
self.standard_code = Some(code);
self
}
}
impl Default for ErrorContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Protocol specification error: {0}")]
Protocol(#[from] ProtocolError),
#[cfg(not(target_arch = "wasm32"))]
#[error("Pipeline processing error: {0}")]
Pipeline(#[from] PipelineError),
#[error("Configuration error: {message}{}", format_context(.context))]
Configuration {
message: String,
context: Box<ErrorContext>,
},
#[error("Validation error: {message}{}", format_context(.context))]
Validation {
message: String,
context: Box<ErrorContext>,
},
#[error("Runtime error: {message}{}", format_context(.context))]
Runtime {
message: String,
context: Box<ErrorContext>,
},
#[cfg(not(target_arch = "wasm32"))]
#[error("Network transport error: {0}")]
Transport(#[from] crate::transport::TransportError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Remote error: HTTP {status} ({class}): {message}{}", format_optional_context(.context))]
Remote {
status: u16,
class: String,
message: String,
retryable: bool,
fallbackable: bool,
retry_after_ms: Option<u32>,
context: Option<Box<ErrorContext>>,
},
#[error("Unknown error: {message}{}", format_context(.context))]
Unknown {
message: String,
context: Box<ErrorContext>,
},
}
fn format_context(ctx: &ErrorContext) -> String {
use std::fmt::Write;
let mut buf = String::new();
let has_meta = ctx.field_path.is_some()
|| ctx.details.is_some()
|| ctx.source.is_some()
|| ctx.request_id.is_some()
|| ctx.status_code.is_some()
|| ctx.error_code.is_some()
|| ctx.retryable.is_some()
|| ctx.fallbackable.is_some()
|| ctx.standard_code.is_some();
if has_meta {
buf.push_str(" [");
let mut first = true;
macro_rules! append_field {
($label:expr, $val:expr) => {
if let Some(ref v) = $val {
if !first {
buf.push_str(", ");
}
let _ = write!(buf, "{}: {}", $label, v);
first = false;
}
};
}
append_field!("field", ctx.field_path);
append_field!("details", ctx.details);
append_field!("source", ctx.source);
append_field!("request_id", ctx.request_id);
if let Some(code) = ctx.status_code {
if !first {
buf.push_str(", ");
}
let _ = write!(buf, "status: {}", code);
first = false;
}
append_field!("error_code", ctx.error_code);
if let Some(r) = ctx.retryable {
if !first {
buf.push_str(", ");
}
let _ = write!(buf, "retryable: {}", r);
first = false;
}
if let Some(f) = ctx.fallbackable {
if !first {
buf.push_str(", ");
}
let _ = write!(buf, "fallbackable: {}", f);
#[allow(unused_assignments)]
{
first = false;
}
}
if let Some(ref std_code) = ctx.standard_code {
if !first {
buf.push_str(", ");
}
let _ = write!(buf, "standard_code: {}", std_code.code());
}
buf.push(']');
}
if let Some(ref hint) = ctx.hint {
let _ = write!(buf, "\n💡 Hint: {}", hint);
}
buf
}
fn format_optional_context(ctx: &Option<Box<ErrorContext>>) -> String {
ctx.as_deref().map(format_context).unwrap_or_default()
}
impl Error {
pub fn runtime_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Runtime {
message: msg.into(),
context: Box::new(context),
}
}
pub fn validation_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Validation {
message: msg.into(),
context: Box::new(context),
}
}
pub fn configuration_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Configuration {
message: msg.into(),
context: Box::new(context),
}
}
pub fn unknown_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Unknown {
message: msg.into(),
context: Box::new(context),
}
}
pub fn validation(msg: impl Into<String>) -> Self {
Self::validation_with_context(msg, ErrorContext::new())
}
pub fn configuration(msg: impl Into<String>) -> Self {
Self::configuration_with_context(msg, ErrorContext::new())
}
pub fn network_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Runtime {
message: format!("Network error: {}", msg.into()),
context: Box::new(context),
}
}
pub fn api_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
Error::Runtime {
message: format!("API error: {}", msg.into()),
context: Box::new(context),
}
}
pub fn parsing(msg: impl Into<String>) -> Self {
Error::Validation {
message: format!("Parsing error: {}", msg.into()),
context: Box::new(ErrorContext::new().with_source("parsing")),
}
}
pub fn context(&self) -> Option<&ErrorContext> {
match self {
Error::Configuration { context, .. }
| Error::Validation { context, .. }
| Error::Runtime { context, .. }
| Error::Unknown { context, .. } => Some(context.as_ref()),
Error::Remote {
context: Some(ref c),
..
} => Some(c.as_ref()),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
match self {
Error::Remote {
retryable, context, ..
} => {
if *retryable {
return true;
}
if let Some(ref ctx) = context {
if let Some(r) = ctx.retryable {
return r;
}
}
self.standard_code().map(|c| c.retryable()).unwrap_or(false)
}
Error::Configuration { context, .. }
| Error::Validation { context, .. }
| Error::Runtime { context, .. }
| Error::Unknown { context, .. } => context
.retryable
.or_else(|| context.standard_code.map(|c| c.retryable()))
.unwrap_or(false),
_ => false,
}
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Error::Remote {
retry_after_ms: Some(ms),
..
} => Some(Duration::from_millis(*ms as u64)),
_ => None,
}
}
#[inline]
pub fn error_code(&self) -> Option<StandardErrorCode> {
self.standard_code()
}
pub fn standard_code(&self) -> Option<StandardErrorCode> {
match self {
Error::Remote {
status,
class,
context,
..
} => context.as_ref().and_then(|c| c.standard_code).or_else(|| {
let from_class = StandardErrorCode::from_error_class(class);
if from_class == StandardErrorCode::Unknown {
Some(StandardErrorCode::from_http_status(*status))
} else {
Some(from_class)
}
}),
Error::Configuration { context, .. }
| Error::Validation { context, .. }
| Error::Runtime { context, .. }
| Error::Unknown { context, .. } => context.standard_code,
_ => None,
}
}
pub fn with_context(mut self, new_ctx: ErrorContext) -> Self {
match &mut self {
Error::Configuration {
ref mut context, ..
}
| Error::Validation {
ref mut context, ..
}
| Error::Runtime {
ref mut context, ..
}
| Error::Unknown {
ref mut context, ..
} => {
**context = new_ctx;
}
Error::Remote {
ref mut context, ..
} => {
*context = Some(Box::new(new_ctx));
}
_ => {}
}
self
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use crate::pipeline::PipelineError as Pipeline;
pub use crate::protocol::ProtocolError as Protocol;