1pub use http::StatusCode;
8use s2_api::v1 as api;
9pub use s2_api::v1::error::ErrorCode;
10
11pub use crate::session::{
12 append::AppendSessionError,
13 read::{CaughtUpError, ReadSessionError},
14};
15use crate::{
16 api::{ApiError, ServerErrorBody},
17 client,
18 types::{FencingToken, StreamPosition, ValidationError},
19};
20
21#[derive(Debug, Clone, thiserror::Error)]
23#[non_exhaustive]
24pub enum ClientError {
25 #[error("connect: {0}")]
27 Connect(String),
28 #[error("timeout")]
30 Timeout,
31 #[error("connection closed early: {0}")]
33 ConnectionClosedEarly(String),
34 #[error("request canceled: {0}")]
36 RequestCanceled(String),
37 #[error("unexpected eof: {0}")]
39 UnexpectedEof(String),
40 #[error("connection reset: {0}")]
42 ConnectionReset(String),
43 #[error("connection aborted: {0}")]
45 ConnectionAborted(String),
46 #[error("connection refused: {0}")]
48 ConnectionRefused(String),
49 #[error("configuration: {0}")]
51 Configuration(String),
52 #[error("request build: {0}")]
54 RequestBuild(String),
55 #[error("request compression: {0}")]
57 RequestCompression(String),
58 #[error("response compression: {0}")]
60 ResponseCompression(String),
61 #[error("response decode: {0}")]
63 ResponseDecode(String),
64 #[error("session protocol: {0}")]
66 SessionProtocol(String),
67 #[error("{0}")]
69 Other(String),
70}
71
72impl ClientError {
73 pub fn is_retryable(&self) -> bool {
75 matches!(
76 self,
77 Self::Connect(_)
78 | Self::Timeout
79 | Self::ConnectionClosedEarly(_)
80 | Self::RequestCanceled(_)
81 | Self::UnexpectedEof(_)
82 | Self::ConnectionReset(_)
83 | Self::ConnectionAborted(_)
84 | Self::ConnectionRefused(_)
85 )
86 }
87
88 pub fn has_no_side_effects(&self) -> bool {
90 matches!(
91 self,
92 Self::Connect(_)
93 | Self::ConnectionRefused(_)
94 | Self::Configuration(_)
95 | Self::RequestBuild(_)
96 | Self::RequestCompression(_)
97 )
98 }
99}
100
101impl From<client::HttpError> for ClientError {
102 fn from(err: client::HttpError) -> Self {
103 let err_msg = err.to_string();
104 match err {
105 client::HttpError::Send(ref send_err) if send_err.is_connect() => {
106 classify_io_source(&err, &err_msg).unwrap_or(Self::Connect(err_msg))
107 }
108 client::HttpError::Send(_) | client::HttpError::Receive(_) => {
109 classify_hyper_source(&err, &err_msg)
110 .or_else(|| classify_io_source(&err, &err_msg))
111 .unwrap_or(Self::Other(err_msg))
112 }
113 client::HttpError::RequestBuild(message) => Self::RequestBuild(message),
114 client::HttpError::RequestCompression(message) => Self::RequestCompression(message),
115 client::HttpError::ResponseCompression(message) => Self::ResponseCompression(message),
116 client::HttpError::ResponseDecode(error) => Self::ResponseDecode(error.to_string()),
117 client::HttpError::Timeout => Self::Timeout,
118 }
119 }
120}
121
122fn classify_hyper_source(err: &client::HttpError, err_msg: &str) -> Option<ClientError> {
123 let hyper_err = source_err::<hyper::Error>(err)?;
124 let err_msg = format!("{hyper_err} -> {err_msg}");
125 if hyper_err.is_timeout() {
126 Some(ClientError::Timeout)
128 } else if hyper_err.is_incomplete_message() || hyper_err.is_closed() {
129 Some(ClientError::ConnectionClosedEarly(err_msg))
132 } else if hyper_err.is_canceled() {
133 Some(ClientError::RequestCanceled(err_msg))
134 } else if source_err::<h2::Error>(err).is_some_and(|e| {
135 e.is_io() || e.is_go_away() || e.reason() == Some(h2::Reason::REFUSED_STREAM)
136 }) {
137 Some(ClientError::ConnectionClosedEarly(err_msg))
141 } else {
142 None
143 }
144}
145
146fn classify_io_source(err: &client::HttpError, err_msg: &str) -> Option<ClientError> {
147 let io_err = source_err::<std::io::Error>(err)?;
148 let err_msg = format!("{io_err} -> {err_msg}");
149 Some(match io_err.kind() {
150 std::io::ErrorKind::UnexpectedEof => ClientError::UnexpectedEof(err_msg),
151 std::io::ErrorKind::BrokenPipe => ClientError::ConnectionClosedEarly(err_msg),
153 std::io::ErrorKind::ConnectionReset => ClientError::ConnectionReset(err_msg),
154 std::io::ErrorKind::ConnectionAborted => ClientError::ConnectionAborted(err_msg),
155 std::io::ErrorKind::ConnectionRefused => ClientError::ConnectionRefused(err_msg),
156 _ => return None,
157 })
158}
159
160fn source_err<T: std::error::Error + 'static>(err: &dyn std::error::Error) -> Option<&T> {
161 let mut source = err.source();
162 while let Some(err) = source {
163 if let Some(err) = err.downcast_ref::<T>() {
164 return Some(err);
165 }
166 source = err.source();
167 }
168 None
169}
170
171#[derive(Debug, Clone, thiserror::Error)]
173#[non_exhaustive]
174pub enum AppendConditionFailed {
175 #[error("fencing token mismatch, expected: {0}")]
177 FencingTokenMismatch(FencingToken),
178 #[error("sequence number mismatch, expected: {0}")]
180 SeqNumMismatch(u64),
181}
182
183impl From<api::stream::AppendConditionFailed> for AppendConditionFailed {
184 fn from(value: api::stream::AppendConditionFailed) -> Self {
185 match value {
186 api::stream::AppendConditionFailed::FencingTokenMismatch(token) => {
187 Self::FencingTokenMismatch(FencingToken::from_server(token.to_string()))
188 }
189 api::stream::AppendConditionFailed::SeqNumMismatch(seq) => Self::SeqNumMismatch(seq),
190 }
191 }
192}
193
194#[derive(Debug, Clone, thiserror::Error)]
196#[non_exhaustive]
197pub enum RequestError {
198 #[error(transparent)]
200 Client(#[from] ClientError),
201 #[error(transparent)]
203 Server(#[from] ServerError),
204 #[error("malformed access token: {0}")]
206 MalformedAccessToken(String),
207 #[cfg(feature = "_hidden")]
208 #[doc(hidden)]
209 #[error("access token provider failed: {0}")]
210 AccessTokenProvider(crate::types::AccessTokenProviderError),
211 #[error(transparent)]
213 Validation(#[from] ValidationError),
214}
215
216impl RequestError {
217 pub fn is_retryable(&self) -> bool {
219 match self {
220 Self::Client(error) => error.is_retryable(),
221 Self::Server(error) => error.is_retryable(),
222 #[cfg(feature = "_hidden")]
223 Self::AccessTokenProvider(error) => error.is_retryable(),
224 Self::MalformedAccessToken(_) | Self::Validation(_) => false,
225 }
226 }
227
228 pub fn has_no_side_effects(&self) -> bool {
230 match self {
231 Self::Client(error) => error.has_no_side_effects(),
232 Self::Server(error) => error.has_no_side_effects(),
233 #[cfg(feature = "_hidden")]
234 Self::AccessTokenProvider(_) => true,
235 Self::MalformedAccessToken(_) | Self::Validation(_) => true,
236 }
237 }
238
239 pub fn server_error(&self) -> Option<&ServerError> {
241 match self {
242 Self::Server(error) => Some(error),
243 _ => None,
244 }
245 }
246
247 pub(crate) fn is_authentication_error(&self) -> bool {
248 matches!(
249 self,
250 Self::Server(error)
251 if error.status == StatusCode::UNAUTHORIZED && error.code == "authn"
252 )
253 }
254
255 pub(crate) fn is_server_draining(&self) -> bool {
256 matches!(
257 self,
258 Self::Server(error)
259 if error.status == StatusCode::SERVICE_UNAVAILABLE
260 && error.code == "server_draining"
261 )
262 }
263}
264
265impl From<ApiError> for RequestError {
266 fn from(error: ApiError) -> Self {
267 match error {
268 ApiError::Client(error) => Self::Client(error),
269 ApiError::ProtoDecode(error) => {
270 Self::Client(ClientError::ResponseDecode(error.to_string()))
271 }
272 ApiError::TerminalDecode(error) => {
273 Self::Client(ClientError::SessionProtocol(error.to_string()))
274 }
275 ApiError::MalformedAccessToken(error) => Self::MalformedAccessToken(error),
276 #[cfg(feature = "_hidden")]
277 ApiError::AccessTokenProvider(error) => Self::AccessTokenProvider(error),
278 ApiError::Compression(error) => {
279 Self::Client(ClientError::ResponseCompression(error.to_string()))
280 }
281 ApiError::Server(status, response) => {
282 Self::Server(ServerError::from_api(status, response))
283 }
284 other => Self::Client(ClientError::Other(other.to_string())),
285 }
286 }
287}
288
289#[derive(Debug, Clone, thiserror::Error)]
291#[non_exhaustive]
292pub enum ReadError {
293 #[error(transparent)]
295 Request(#[from] RequestError),
296 #[error("read from an unwritten position. current tail: {0}")]
298 ReadUnwritten(StreamPosition),
299}
300
301impl ReadError {
302 pub fn is_retryable(&self) -> bool {
304 matches!(self, Self::Request(error) if error.is_retryable())
305 }
306
307 pub fn request_error(&self) -> Option<&RequestError> {
309 match self {
310 Self::Request(error) => Some(error),
311 Self::ReadUnwritten(_) => None,
312 }
313 }
314}
315
316impl From<ApiError> for ReadError {
317 fn from(error: ApiError) -> Self {
318 match error {
319 ApiError::ReadUnwritten(tail) => Self::ReadUnwritten(tail.tail.into()),
320 other => Self::Request(other.into()),
321 }
322 }
323}
324
325#[derive(Debug, Clone, thiserror::Error)]
327#[non_exhaustive]
328pub enum AppendError {
329 #[error(transparent)]
331 Request(#[from] RequestError),
332 #[error(transparent)]
334 ConditionFailed(#[from] AppendConditionFailed),
335}
336
337impl AppendError {
338 pub fn is_retryable(&self) -> bool {
340 matches!(self, Self::Request(error) if error.is_retryable())
341 }
342
343 pub fn has_no_side_effects(&self) -> bool {
345 match self {
346 Self::Request(error) => error.has_no_side_effects(),
347 Self::ConditionFailed(_) => true,
348 }
349 }
350
351 pub fn request_error(&self) -> Option<&RequestError> {
353 match self {
354 Self::Request(error) => Some(error),
355 Self::ConditionFailed(_) => None,
356 }
357 }
358}
359
360impl From<ApiError> for AppendError {
361 fn from(error: ApiError) -> Self {
362 match error {
363 ApiError::AppendConditionFailed(condition) => Self::ConditionFailed(condition.into()),
364 other => Self::Request(other.into()),
365 }
366 }
367}
368
369#[derive(Debug, Clone, thiserror::Error)]
371#[non_exhaustive]
372pub enum ProducerError {
373 #[error(transparent)]
375 Append(#[from] AppendSessionError),
376 #[error(transparent)]
378 Validation(#[from] ValidationError),
379 #[error("producer already closed")]
381 ProducerClosed,
382 #[error("producer is closing")]
384 ProducerClosing,
385 #[error("producer dropped without calling close")]
387 ProducerDropped,
388}
389
390impl ProducerError {
391 pub fn is_retryable(&self) -> bool {
393 match self {
394 Self::Append(error) => error.is_retryable(),
395 Self::Validation(_)
396 | Self::ProducerClosed
397 | Self::ProducerClosing
398 | Self::ProducerDropped => false,
399 }
400 }
401
402 pub fn has_no_side_effects(&self) -> bool {
404 match self {
405 Self::Append(error) => error.has_no_side_effects(),
406 Self::Validation(_) | Self::ProducerClosed | Self::ProducerClosing => true,
407 Self::ProducerDropped => false,
408 }
409 }
410
411 pub fn request_error(&self) -> Option<&RequestError> {
413 match self {
414 Self::Append(error) => error.request_error(),
415 Self::Validation(_)
416 | Self::ProducerClosed
417 | Self::ProducerClosing
418 | Self::ProducerDropped => None,
419 }
420 }
421}
422
423#[derive(Debug, Clone, thiserror::Error)]
425#[error("{code}: {message}")]
426#[non_exhaustive]
427pub struct ServerError {
428 pub status: StatusCode,
430 pub code: String,
432 pub message: String,
434}
435
436impl ServerError {
437 pub(crate) fn from_api(status: StatusCode, response: ServerErrorBody) -> Self {
438 Self {
439 status,
440 code: response.code,
441 message: response.message,
442 }
443 }
444
445 pub fn known_code(&self) -> Option<ErrorCode> {
450 self.code.parse().ok()
451 }
452
453 pub fn is_retryable(&self) -> bool {
455 server_error_is_retryable(self.status, &self.code)
456 }
457
458 pub fn has_no_side_effects(&self) -> bool {
460 server_error_has_no_side_effects(self.status, &self.code)
461 }
462}
463
464pub(crate) fn server_error_is_retryable(status: StatusCode, code: &str) -> bool {
465 match code.parse::<ErrorCode>() {
466 Ok(code) if code.status() == status => code.is_retryable(),
467 Ok(_) => false,
468 Err(_) => matches!(
469 status,
470 StatusCode::REQUEST_TIMEOUT
471 | StatusCode::TOO_MANY_REQUESTS
472 | StatusCode::INTERNAL_SERVER_ERROR
473 | StatusCode::BAD_GATEWAY
474 | StatusCode::SERVICE_UNAVAILABLE
475 | StatusCode::GATEWAY_TIMEOUT
476 ),
477 }
478}
479
480pub(crate) fn server_error_has_no_side_effects(status: StatusCode, code: &str) -> bool {
481 code.parse::<ErrorCode>()
482 .is_ok_and(|code| code.status() == status && code.has_no_side_effects())
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 fn response(status: StatusCode, code: &str) -> ServerError {
490 ServerError::from_api(
491 status,
492 ServerErrorBody {
493 code: code.to_owned(),
494 message: "test".to_owned(),
495 },
496 )
497 }
498
499 #[test]
500 fn error_response_preserves_raw_and_known_codes() {
501 let known = response(StatusCode::NOT_FOUND, "basin_not_found");
502 assert_eq!(known.code, "basin_not_found");
503 assert_eq!(known.message, "test");
504 assert_eq!(known.known_code(), Some(ErrorCode::BasinNotFound));
505 assert!(known.to_string().contains("basin_not_found"));
506
507 let unknown = response(StatusCode::BAD_REQUEST, "introduced_by_a_newer_server");
508 assert_eq!(unknown.known_code(), None);
509 assert_eq!(unknown.code, "introduced_by_a_newer_server");
510 }
511
512 #[test]
513 fn server_classification_fails_closed_on_status_mismatch() {
514 let mismatch = response(StatusCode::INTERNAL_SERVER_ERROR, "rate_limited");
515 assert!(!mismatch.is_retryable());
516 assert!(!mismatch.has_no_side_effects());
517 }
518
519 #[test]
520 fn unknown_codes_retain_retryable_status_fallback() {
521 let unknown = response(StatusCode::SERVICE_UNAVAILABLE, "future_server_error");
522 assert!(unknown.is_retryable());
523 assert!(!unknown.has_no_side_effects());
524 }
525
526 #[test]
527 fn internal_client_errors_preserve_the_failure_stage() {
528 assert!(matches!(
529 ClientError::from(client::HttpError::RequestBuild("bad request".to_owned())),
530 ClientError::RequestBuild(message) if message == "bad request"
531 ));
532 assert!(matches!(
533 ClientError::from(client::HttpError::RequestCompression("encode".to_owned())),
534 ClientError::RequestCompression(message) if message == "encode"
535 ));
536 assert!(matches!(
537 ClientError::from(client::HttpError::ResponseCompression("decode".to_owned())),
538 ClientError::ResponseCompression(message) if message == "decode"
539 ));
540
541 let json_error = serde_json::from_slice::<serde_json::Value>(b"{")
542 .expect_err("invalid JSON should fail");
543 assert!(matches!(
544 ClientError::from(client::HttpError::ResponseDecode(json_error)),
545 ClientError::ResponseDecode(_)
546 ));
547 }
548
549 #[test]
550 fn nested_errors_expose_request_and_server_errors() {
551 let append = AppendError::Request(RequestError::Server(response(
552 StatusCode::CONFLICT,
553 "transaction_conflict",
554 )));
555
556 assert!(append.is_retryable());
557 assert!(append.has_no_side_effects());
558 let request = append.request_error().expect("request error");
559 assert!(matches!(request, RequestError::Server(_)));
560 let server = request.server_error().expect("server error");
561 assert_eq!(server.known_code(), Some(ErrorCode::TransactionConflict));
562 }
563}