1use crate::error_code::StandardErrorCode;
44use crate::pipeline::PipelineError;
45use crate::protocol::ProtocolError;
46use std::time::Duration;
47use thiserror::Error;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ErrorContext {
55 pub field_path: Option<String>,
58 pub details: Option<String>,
60 pub source: Option<String>,
62 pub hint: Option<String>,
64 pub request_id: Option<String>,
66 pub status_code: Option<u16>,
68 pub error_code: Option<String>,
70 pub retryable: Option<bool>,
72 pub fallbackable: Option<bool>,
74 pub standard_code: Option<StandardErrorCode>,
76}
77
78impl ErrorContext {
79 pub fn new() -> Self {
80 Self {
81 field_path: None,
82 details: None,
83 source: None,
84 hint: None,
85 request_id: None,
86 status_code: None,
87 error_code: None,
88 retryable: None,
89 fallbackable: None,
90 standard_code: None,
91 }
92 }
93
94 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
95 self.field_path = Some(path.into());
96 self
97 }
98
99 pub fn with_details(mut self, details: impl Into<String>) -> Self {
100 self.details = Some(details.into());
101 self
102 }
103
104 pub fn with_source(mut self, source: impl Into<String>) -> Self {
105 self.source = Some(source.into());
106 self
107 }
108
109 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
110 self.hint = Some(hint.into());
111 self
112 }
113
114 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
115 self.request_id = Some(id.into());
116 self
117 }
118
119 pub fn with_status_code(mut self, code: u16) -> Self {
120 self.status_code = Some(code);
121 self
122 }
123
124 pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
125 self.error_code = Some(code.into());
126 self
127 }
128
129 pub fn with_retryable(mut self, retryable: bool) -> Self {
130 self.retryable = Some(retryable);
131 self
132 }
133
134 pub fn with_fallbackable(mut self, fallbackable: bool) -> Self {
135 self.fallbackable = Some(fallbackable);
136 self
137 }
138
139 pub fn with_standard_code(mut self, code: StandardErrorCode) -> Self {
140 self.standard_code = Some(code);
141 self
142 }
143}
144
145impl Default for ErrorContext {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[derive(Debug, Error)]
154pub enum Error {
155 #[error("Protocol specification error: {0}")]
156 Protocol(#[from] ProtocolError),
157
158 #[error("Pipeline processing error: {0}")]
159 Pipeline(#[from] PipelineError),
160
161 #[error("Configuration error: {message}{}", format_context(.context))]
162 Configuration {
163 message: String,
164 context: ErrorContext,
165 },
166
167 #[error("Validation error: {message}{}", format_context(.context))]
168 Validation {
169 message: String,
170 context: ErrorContext,
171 },
172
173 #[error("Runtime error: {message}{}", format_context(.context))]
174 Runtime {
175 message: String,
176 context: ErrorContext,
177 },
178
179 #[error("Network transport error: {0}")]
180 Transport(#[from] crate::transport::TransportError),
181
182 #[error("I/O error: {0}")]
183 Io(#[from] std::io::Error),
184
185 #[error("Serialization error: {0}")]
186 Serialization(#[from] serde_json::Error),
187
188 #[error("Remote error: HTTP {status} ({class}): {message}{}", format_optional_context(.context))]
189 Remote {
190 status: u16,
191 class: String,
192 message: String,
193 retryable: bool,
194 fallbackable: bool,
195 retry_after_ms: Option<u32>,
196 context: Option<ErrorContext>,
197 },
198
199 #[error("Unknown error: {message}{}", format_context(.context))]
200 Unknown {
201 message: String,
202 context: ErrorContext,
203 },
204}
205
206fn format_context(ctx: &ErrorContext) -> String {
209 use std::fmt::Write;
210 let mut buf = String::new();
211
212 let has_meta = ctx.field_path.is_some()
214 || ctx.details.is_some()
215 || ctx.source.is_some()
216 || ctx.request_id.is_some()
217 || ctx.status_code.is_some()
218 || ctx.error_code.is_some()
219 || ctx.retryable.is_some()
220 || ctx.fallbackable.is_some()
221 || ctx.standard_code.is_some();
222
223 if has_meta {
224 buf.push_str(" [");
225 let mut first = true;
226 macro_rules! append_field {
227 ($label:expr, $val:expr) => {
228 if let Some(ref v) = $val {
229 if !first { buf.push_str(", "); }
230 let _ = write!(buf, "{}: {}", $label, v);
231 first = false;
232 }
233 };
234 }
235 append_field!("field", ctx.field_path);
236 append_field!("details", ctx.details);
237 append_field!("source", ctx.source);
238 append_field!("request_id", ctx.request_id);
239 if let Some(code) = ctx.status_code {
240 if !first { buf.push_str(", "); }
241 let _ = write!(buf, "status: {}", code);
242 first = false;
243 }
244 append_field!("error_code", ctx.error_code);
245 if let Some(r) = ctx.retryable {
246 if !first { buf.push_str(", "); }
247 let _ = write!(buf, "retryable: {}", r);
248 first = false;
249 }
250 if let Some(f) = ctx.fallbackable {
251 if !first { buf.push_str(", "); }
252 let _ = write!(buf, "fallbackable: {}", f);
253 #[allow(unused_assignments)]
254 { first = false; }
255 }
256 if let Some(ref std_code) = ctx.standard_code {
257 if !first { buf.push_str(", "); }
258 let _ = write!(buf, "standard_code: {}", std_code.code());
259 }
260 buf.push(']');
261 }
262
263 if let Some(ref hint) = ctx.hint {
264 let _ = write!(buf, "\n💡 Hint: {}", hint);
265 }
266
267 buf
268}
269
270fn format_optional_context(ctx: &Option<ErrorContext>) -> String {
271 ctx.as_ref().map(format_context).unwrap_or_default()
272}
273
274impl Error {
275 pub fn runtime_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
277 Error::Runtime {
278 message: msg.into(),
279 context,
280 }
281 }
282
283 pub fn validation_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
285 Error::Validation {
286 message: msg.into(),
287 context,
288 }
289 }
290
291 pub fn configuration_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
293 Error::Configuration {
294 message: msg.into(),
295 context,
296 }
297 }
298
299 pub fn unknown_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
301 Error::Unknown {
302 message: msg.into(),
303 context,
304 }
305 }
306
307 pub fn validation(msg: impl Into<String>) -> Self {
309 Self::validation_with_context(msg, ErrorContext::new())
310 }
311
312 pub fn configuration(msg: impl Into<String>) -> Self {
314 Self::configuration_with_context(msg, ErrorContext::new())
315 }
316
317 pub fn network_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
319 Error::Runtime {
320 message: format!("Network error: {}", msg.into()),
321 context,
322 }
323 }
324
325 pub fn api_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
327 Error::Runtime {
328 message: format!("API error: {}", msg.into()),
329 context,
330 }
331 }
332
333 pub fn parsing(msg: impl Into<String>) -> Self {
335 Error::Validation {
336 message: format!("Parsing error: {}", msg.into()),
337 context: ErrorContext::new().with_source("parsing"),
338 }
339 }
340
341 pub fn context(&self) -> Option<&ErrorContext> {
343 match self {
344 Error::Configuration { context, .. }
345 | Error::Validation { context, .. }
346 | Error::Runtime { context, .. }
347 | Error::Unknown { context, .. } => Some(context),
348 Error::Remote {
349 context: Some(ref c),
350 ..
351 } => Some(c),
352 _ => None,
353 }
354 }
355
356 pub fn is_retryable(&self) -> bool {
361 match self {
362 Error::Remote { retryable, context, .. } => {
363 if *retryable {
364 return true;
365 }
366 if let Some(ref ctx) = context {
367 if let Some(r) = ctx.retryable {
368 return r;
369 }
370 }
371 self.standard_code().map(|c| c.retryable()).unwrap_or(false)
372 }
373 Error::Configuration { context, .. }
374 | Error::Validation { context, .. }
375 | Error::Runtime { context, .. }
376 | Error::Unknown { context, .. } => context
377 .retryable
378 .or_else(|| context.standard_code.map(|c| c.retryable()))
379 .unwrap_or(false),
380 _ => false,
381 }
382 }
383
384 pub fn retry_after(&self) -> Option<Duration> {
388 match self {
389 Error::Remote {
390 retry_after_ms: Some(ms),
391 ..
392 } => Some(Duration::from_millis(*ms as u64)),
393 _ => None,
394 }
395 }
396
397 #[inline]
401 pub fn error_code(&self) -> Option<StandardErrorCode> {
402 self.standard_code()
403 }
404
405 pub fn standard_code(&self) -> Option<StandardErrorCode> {
410 match self {
411 Error::Remote {
412 status,
413 class,
414 context,
415 ..
416 } => context
417 .as_ref()
418 .and_then(|c| c.standard_code)
419 .or_else(|| {
420 let from_class = StandardErrorCode::from_error_class(class);
422 if from_class == StandardErrorCode::Unknown {
423 Some(StandardErrorCode::from_http_status(*status))
424 } else {
425 Some(from_class)
426 }
427 }),
428 Error::Configuration { context, .. }
429 | Error::Validation { context, .. }
430 | Error::Runtime { context, .. }
431 | Error::Unknown { context, .. } => context.standard_code,
432 _ => None,
433 }
434 }
435
436 pub fn with_context(mut self, new_ctx: ErrorContext) -> Self {
438 match &mut self {
439 Error::Configuration {
440 ref mut context, ..
441 }
442 | Error::Validation {
443 ref mut context, ..
444 }
445 | Error::Runtime {
446 ref mut context, ..
447 }
448 | Error::Unknown {
449 ref mut context, ..
450 } => {
451 *context = new_ctx;
452 }
453 Error::Remote {
454 ref mut context, ..
455 } => {
456 *context = Some(new_ctx);
457 }
458 _ => {}
459 }
460 self
461 }
462}
463
464pub use crate::pipeline::PipelineError as Pipeline;
466pub use crate::protocol::ProtocolError as Protocol;