Skip to main content

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}