docling/error.rs
1//! Error type for conversion.
2
3use std::fmt;
4
5use crate::format::InputFormat;
6
7/// Anything that can go wrong while loading or converting a source document.
8#[derive(Debug)]
9pub enum ConversionError {
10 /// Reading the input from disk failed.
11 Io(std::io::Error),
12 /// The file extension (or content) did not map to a known format.
13 UnknownFormat { hint: String },
14 /// The format is known but no backend is wired up for it yet.
15 UnsupportedFormat(InputFormat),
16 /// The backend recognized the format but failed to parse the content.
17 Parse(String),
18 /// The requested streaming conversion is not supported (e.g. JSON, or the
19 /// referenced image mode, which both need the whole document up front).
20 Streaming(String),
21 /// The headless-browser pre-render (`--use-web-browser`) failed, or the crate
22 /// was built without the `web-browser` feature.
23 Browser(String),
24 /// A conversion worker panicked — a backend bug reached on some input
25 /// (#395/#396). The panic itself is not swallowed: it still unwinds its own
26 /// thread and prints its message and backtrace to stderr. This turns it
27 /// into an ordinary error for the caller, so a server answers with a 500
28 /// instead of a silent empty document and a batch keeps going.
29 Panic(String),
30 /// The document budget (`DocumentConverter::document_timeout`, #497) ran
31 /// out on a **streaming** conversion. Not a failure: every chunk emitted
32 /// before it is the partial document, and this is the stream's last item
33 /// — the streaming counterpart of [`crate::ConversionStatus::PartialSuccess`]
34 /// with a timeout [`crate::ErrorItem`]. Buffered conversions never raise
35 /// it; they report the cut on the result.
36 Timeout(String),
37 /// A dependency failed during conversion. Unlike [`ConversionError::Parse`]
38 /// the underlying error is kept alive (not flattened into a string), so
39 /// callers can walk [`std::error::Error::source`] and downcast to the
40 /// original type — e.g. to tell a truncated archive from malformed XML.
41 WithSource {
42 /// What was being converted when the failure happened (backend prefix,
43 /// mirroring the `Parse` message style: "xlsx", "docling-json", …).
44 context: String,
45 /// The error that caused the failure, preserved on the chain.
46 source: Box<dyn std::error::Error + Send + Sync>,
47 },
48}
49
50impl ConversionError {
51 /// Wrap a dependency error, keeping it reachable via
52 /// [`std::error::Error::source`] instead of stringifying it.
53 pub fn with_source(
54 context: impl Into<String>,
55 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
56 ) -> Self {
57 ConversionError::WithSource {
58 context: context.into(),
59 source: source.into(),
60 }
61 }
62}
63
64impl fmt::Display for ConversionError {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 ConversionError::Io(e) => write!(f, "i/o error: {e}"),
68 ConversionError::UnknownFormat { hint } => {
69 write!(f, "could not determine input format (hint: {hint})")
70 }
71 ConversionError::UnsupportedFormat(fmt) => {
72 write!(
73 f,
74 "no backend implemented yet for format '{}'",
75 fmt.as_str()
76 )
77 }
78 ConversionError::Parse(msg) => write!(f, "parse error: {msg}"),
79 ConversionError::Streaming(msg) => write!(f, "streaming not supported: {msg}"),
80 ConversionError::Browser(msg) => write!(f, "web-browser render error: {msg}"),
81 ConversionError::Panic(msg) => write!(f, "conversion panicked: {msg}"),
82 ConversionError::Timeout(msg) => write!(f, "document timeout: {msg}"),
83 ConversionError::WithSource { context, source } => {
84 write!(f, "parse error: {context}: {source}")
85 }
86 }
87 }
88}
89
90impl std::error::Error for ConversionError {
91 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92 match self {
93 ConversionError::Io(e) => Some(e),
94 ConversionError::WithSource { source, .. } => Some(source.as_ref()),
95 _ => None,
96 }
97 }
98}
99
100impl From<std::io::Error> for ConversionError {
101 fn from(e: std::io::Error) -> Self {
102 ConversionError::Io(e)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::error::Error;
110
111 #[test]
112 fn with_source_keeps_the_cause_on_the_chain() {
113 let cause = serde_json::from_str::<i32>("boom").unwrap_err();
114 let err = ConversionError::with_source("docling-json", cause);
115
116 assert!(err.to_string().starts_with("parse error: docling-json: "));
117 let source = err.source().expect("source is chained");
118 assert!(
119 source.downcast_ref::<serde_json::Error>().is_some(),
120 "chained source downcasts to the original type"
121 );
122 }
123
124 #[test]
125 fn stringly_variants_have_no_source() {
126 assert!(ConversionError::Parse("x".into()).source().is_none());
127 }
128}