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 #[error(
338 "append may have taken effect in an earlier attempt; final attempt failed: {final_attempt_error}"
339 )]
340 IndefiniteFailure {
341 #[source]
343 final_attempt_error: Box<Self>,
344 },
345}
346
347impl AppendError {
348 pub fn is_retryable(&self) -> bool {
350 match self {
351 Self::Request(error) => error.is_retryable(),
352 Self::ConditionFailed(_) => false,
353 Self::IndefiniteFailure {
354 final_attempt_error,
355 } => final_attempt_error.is_retryable(),
356 }
357 }
358
359 pub fn has_no_side_effects(&self) -> bool {
361 match self {
362 Self::Request(error) => error.has_no_side_effects(),
363 Self::ConditionFailed(_) => true,
364 Self::IndefiniteFailure { .. } => false,
365 }
366 }
367
368 pub fn request_error(&self) -> Option<&RequestError> {
370 match self {
371 Self::Request(error) => Some(error),
372 Self::ConditionFailed(_) => None,
373 Self::IndefiniteFailure {
374 final_attempt_error,
375 } => final_attempt_error.request_error(),
376 }
377 }
378}
379
380impl From<ApiError> for AppendError {
381 fn from(error: ApiError) -> Self {
382 match error {
383 ApiError::AppendConditionFailed(condition) => Self::ConditionFailed(condition.into()),
384 ApiError::IndefiniteFailure {
385 final_attempt_error,
386 } => Self::IndefiniteFailure {
387 final_attempt_error: Box::new((*final_attempt_error).into()),
388 },
389 other => Self::Request(other.into()),
390 }
391 }
392}
393
394#[derive(Debug, Clone, thiserror::Error)]
396#[non_exhaustive]
397pub enum ProducerError {
398 #[error(transparent)]
400 Append(#[from] AppendSessionError),
401 #[error(transparent)]
403 Validation(#[from] ValidationError),
404 #[error("producer already closed")]
406 ProducerClosed,
407 #[error("producer is closing")]
409 ProducerClosing,
410 #[error("producer dropped without calling close")]
412 ProducerDropped,
413}
414
415impl ProducerError {
416 pub fn is_retryable(&self) -> bool {
418 match self {
419 Self::Append(error) => error.is_retryable(),
420 Self::Validation(_)
421 | Self::ProducerClosed
422 | Self::ProducerClosing
423 | Self::ProducerDropped => false,
424 }
425 }
426
427 pub fn has_no_side_effects(&self) -> bool {
429 match self {
430 Self::Append(error) => error.has_no_side_effects(),
431 Self::Validation(_) | Self::ProducerClosed | Self::ProducerClosing => true,
432 Self::ProducerDropped => false,
433 }
434 }
435
436 pub fn request_error(&self) -> Option<&RequestError> {
438 match self {
439 Self::Append(error) => error.request_error(),
440 Self::Validation(_)
441 | Self::ProducerClosed
442 | Self::ProducerClosing
443 | Self::ProducerDropped => None,
444 }
445 }
446}
447
448#[derive(Debug, Clone, thiserror::Error)]
450#[error("{code}: {message}")]
451#[non_exhaustive]
452pub struct ServerError {
453 pub status: StatusCode,
455 pub code: String,
457 pub message: String,
459}
460
461impl ServerError {
462 pub(crate) fn from_api(status: StatusCode, response: ServerErrorBody) -> Self {
463 Self {
464 status,
465 code: response.code,
466 message: response.message,
467 }
468 }
469
470 pub fn known_code(&self) -> Option<ErrorCode> {
475 self.code.parse().ok()
476 }
477
478 pub fn is_retryable(&self) -> bool {
480 server_error_is_retryable(self.status, &self.code)
481 }
482
483 pub fn has_no_side_effects(&self) -> bool {
485 server_error_has_no_side_effects(self.status, &self.code)
486 }
487}
488
489pub(crate) fn server_error_is_retryable(status: StatusCode, code: &str) -> bool {
490 match code.parse::<ErrorCode>() {
491 Ok(code) if code.status() == status => code.is_retryable(),
492 Ok(_) => false,
493 Err(_) => matches!(
494 status,
495 StatusCode::REQUEST_TIMEOUT
496 | StatusCode::TOO_MANY_REQUESTS
497 | StatusCode::INTERNAL_SERVER_ERROR
498 | StatusCode::BAD_GATEWAY
499 | StatusCode::SERVICE_UNAVAILABLE
500 | StatusCode::GATEWAY_TIMEOUT
501 ),
502 }
503}
504
505pub(crate) fn server_error_has_no_side_effects(status: StatusCode, code: &str) -> bool {
506 code.parse::<ErrorCode>()
507 .is_ok_and(|code| code.status() == status && code.has_no_side_effects())
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 fn response(status: StatusCode, code: &str) -> ServerError {
515 ServerError::from_api(
516 status,
517 ServerErrorBody {
518 code: code.to_owned(),
519 message: "test".to_owned(),
520 },
521 )
522 }
523
524 #[test]
525 fn error_response_preserves_raw_and_known_codes() {
526 let known = response(StatusCode::NOT_FOUND, "basin_not_found");
527 assert_eq!(known.code, "basin_not_found");
528 assert_eq!(known.message, "test");
529 assert_eq!(known.known_code(), Some(ErrorCode::BasinNotFound));
530 assert!(known.to_string().contains("basin_not_found"));
531
532 let unknown = response(StatusCode::BAD_REQUEST, "introduced_by_a_newer_server");
533 assert_eq!(unknown.known_code(), None);
534 assert_eq!(unknown.code, "introduced_by_a_newer_server");
535 }
536
537 #[test]
538 fn server_classification_fails_closed_on_status_mismatch() {
539 let mismatch = response(StatusCode::INTERNAL_SERVER_ERROR, "rate_limited");
540 assert!(!mismatch.is_retryable());
541 assert!(!mismatch.has_no_side_effects());
542 }
543
544 #[test]
545 fn unknown_codes_retain_retryable_status_fallback() {
546 let unknown = response(StatusCode::SERVICE_UNAVAILABLE, "future_server_error");
547 assert!(unknown.is_retryable());
548 assert!(!unknown.has_no_side_effects());
549 }
550
551 #[test]
552 fn internal_client_errors_preserve_the_failure_stage() {
553 assert!(matches!(
554 ClientError::from(client::HttpError::RequestBuild("bad request".to_owned())),
555 ClientError::RequestBuild(message) if message == "bad request"
556 ));
557 assert!(matches!(
558 ClientError::from(client::HttpError::RequestCompression("encode".to_owned())),
559 ClientError::RequestCompression(message) if message == "encode"
560 ));
561 assert!(matches!(
562 ClientError::from(client::HttpError::ResponseCompression("decode".to_owned())),
563 ClientError::ResponseCompression(message) if message == "decode"
564 ));
565
566 let json_error = serde_json::from_slice::<serde_json::Value>(b"{")
567 .expect_err("invalid JSON should fail");
568 assert!(matches!(
569 ClientError::from(client::HttpError::ResponseDecode(json_error)),
570 ClientError::ResponseDecode(_)
571 ));
572 }
573
574 #[test]
575 fn nested_errors_expose_request_and_server_errors() {
576 let append = AppendError::Request(RequestError::Server(response(
577 StatusCode::CONFLICT,
578 "transaction_conflict",
579 )));
580
581 assert!(append.is_retryable());
582 assert!(append.has_no_side_effects());
583 let request = append.request_error().expect("request error");
584 assert!(matches!(request, RequestError::Server(_)));
585 let server = request.server_error().expect("server error");
586 assert_eq!(server.known_code(), Some(ErrorCode::TransactionConflict));
587 }
588}