Skip to main content

ruststream_rdkafka/
error.rs

1//! The crate error type shared by the broker, publishers, and subscribers.
2
3use std::error::Error as StdError;
4#[cfg(feature = "schema-registry")]
5use std::time::Duration;
6
7use thiserror::Error;
8
9/// Errors returned by [`KafkaBroker`](crate::KafkaBroker) and the types it hands out.
10///
11/// Underlying [`rdkafka`](https://docs.rs/rdkafka) errors are boxed as sources so the client
12/// library does not leak into this crate's public API surface.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum KafkaError {
16    /// Creating a client failed or the cluster was unreachable during the connect probe.
17    #[error("kafka connection error: {0}")]
18    Connect(#[source] Box<dyn StdError + Send + Sync>),
19
20    /// Publishing a message failed or the broker did not confirm its delivery.
21    #[error("kafka publish error: {0}")]
22    Publish(#[source] Box<dyn StdError + Send + Sync>),
23
24    /// Creating a consumer or subscribing it to its topic failed.
25    #[error("kafka subscribe error: {0}")]
26    Subscribe(#[source] Box<dyn StdError + Send + Sync>),
27
28    /// Receiving a delivery from an open consumer failed.
29    #[error("kafka consume error: {0}")]
30    Consume(#[source] Box<dyn StdError + Send + Sync>),
31
32    /// A handle aliasing the connection was used after the broker shut down.
33    ///
34    /// The lifecycle ladder makes misuse through the owner's handle a compile error:
35    /// [`ConnectedBroker::shutdown`](ruststream::ConnectedBroker::shutdown) consumes the
36    /// connected broker. Publishers paired off it earlier, and subscriptions still open, keep
37    /// aliasing the closed connection, so their operations report this instead of silently
38    /// succeeding against a dead connection.
39    #[error("kafka connection is closed; cannot reach {topic}")]
40    Closed {
41        /// The topic the operation targeted, or the transactional id of a transaction control
42        /// call.
43        topic: String,
44    },
45
46    /// The requested combination of options cannot be executed.
47    ///
48    /// The message names the offending option and the remediation.
49    #[error("invalid options: {0}")]
50    InvalidOptions(String),
51
52    /// `begin_transaction` found a transaction already open on this publisher.
53    ///
54    /// One producer runs one transaction at a time, so a second begin means two flows share
55    /// one publisher; erroring beats silently merging their messages into one transaction.
56    /// Concurrent transactional flows need distinct publishers - one per partition via
57    /// [`TransactionalPartitions`](crate::TransactionalPartitions), or distinct explicit ids.
58    #[error(
59        "a transaction is already open on publisher {id}; one publisher runs one transaction \
60         at a time - use distinct publishers (for example the per-partition set) for \
61         concurrent transactional flows"
62    )]
63    TransactionBusy {
64        /// The transactional id of the publisher that already has an open transaction.
65        id: String,
66    },
67
68    /// `commit` or `abort` was called with no transaction open on this publisher.
69    #[error("no transaction is open on publisher {id}; `begin_transaction` opens one")]
70    NoTransaction {
71        /// The transactional id of the publisher the call was made on.
72        id: String,
73    },
74
75    /// A Schema Registry request failed: unreachable registry, rejected credentials, an
76    /// unknown schema id or subject, or a schema the registry refused.
77    #[cfg(feature = "schema-registry")]
78    #[error("schema registry error: {0}")]
79    SchemaRegistry(#[source] Box<dyn StdError + Send + Sync>),
80
81    /// A Schema Registry request did not answer within the client's request timeout.
82    ///
83    /// A registry that accepts the connection and then goes silent is the case this separates
84    /// from an outright failure: both edges resolve schemas on the async delivery path, so
85    /// without a deadline one silent registry holds a delivery for as long as it stays silent.
86    /// [`SchemaRegistry::request_timeout`](crate::SchemaRegistry::request_timeout) sets the
87    /// deadline.
88    #[cfg(feature = "schema-registry")]
89    #[error("schema registry request {request} did not answer within {timeout:?}")]
90    SchemaRegistryTimeout {
91        /// The registry path the request targeted, for example `/schemas/ids/7`.
92        request: String,
93        /// The deadline that expired.
94        timeout: Duration,
95    },
96
97    /// A value could not be written to, or read from, its binary wire form.
98    ///
99    /// This is the byte lanes' own failure: the Avro or Protobuf encoder rejected the value,
100    /// the payload is not the datum a schema describes, or a delivery does not carry the
101    /// Confluent envelope the reader expected. It stays distinct from
102    /// [`SchemaRegistry`](Self::SchemaRegistry), which is the registry conversation failing -
103    /// the fixed-schema lane reaches no registry at all, so folding the two would make every
104    /// diagnostic point at a component that was never involved.
105    #[cfg(feature = "schema-registry")]
106    #[error("wire format error: {0}")]
107    WireFormat(#[source] Box<dyn StdError + Send + Sync>),
108}
109
110impl KafkaError {
111    pub(crate) fn connect(err: rdkafka::error::KafkaError) -> Self {
112        Self::Connect(Box::new(err))
113    }
114
115    pub(crate) fn publish(err: rdkafka::error::KafkaError) -> Self {
116        Self::Publish(Box::new(err))
117    }
118
119    #[cfg(feature = "schema-registry")]
120    pub(crate) fn schema_registry(err: impl StdError + Send + Sync + 'static) -> Self {
121        Self::SchemaRegistry(Box::new(err))
122    }
123
124    // Only a format's own encoder produces one of these; the envelope's own failures come
125    // through `malformed`, which the frame types report with no format feature enabled.
126    #[cfg(any(feature = "avro", feature = "protobuf"))]
127    pub(crate) fn wire_format(err: impl StdError + Send + Sync + 'static) -> Self {
128        Self::WireFormat(Box::new(err))
129    }
130
131    /// The wire-format failure the lanes report for a payload that is structurally wrong (not
132    /// framed, indexes truncated) rather than for another error's failure.
133    #[cfg(feature = "schema-registry")]
134    pub(crate) fn malformed(message: impl Into<String>) -> Self {
135        Self::WireFormat(message.into().into())
136    }
137
138    pub(crate) fn subscribe(err: rdkafka::error::KafkaError) -> Self {
139        Self::Subscribe(Box::new(err))
140    }
141
142    pub(crate) fn consume(err: rdkafka::error::KafkaError) -> Self {
143        Self::Consume(Box::new(err))
144    }
145}