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_incomplete_message() {
126 Some(ClientError::ConnectionClosedEarly(err_msg))
127 } else if hyper_err.is_canceled() {
128 Some(ClientError::RequestCanceled(err_msg))
129 } else {
130 None
131 }
132}
133
134fn classify_io_source(err: &client::HttpError, err_msg: &str) -> Option<ClientError> {
135 let io_err = source_err::<std::io::Error>(err)?;
136 let err_msg = format!("{io_err} -> {err_msg}");
137 Some(match io_err.kind() {
138 std::io::ErrorKind::UnexpectedEof => ClientError::UnexpectedEof(err_msg),
139 std::io::ErrorKind::ConnectionReset => ClientError::ConnectionReset(err_msg),
140 std::io::ErrorKind::ConnectionAborted => ClientError::ConnectionAborted(err_msg),
141 std::io::ErrorKind::ConnectionRefused => ClientError::ConnectionRefused(err_msg),
142 _ => return None,
143 })
144}
145
146fn source_err<T: std::error::Error + 'static>(err: &dyn std::error::Error) -> Option<&T> {
147 let mut source = err.source();
148 while let Some(err) = source {
149 if let Some(err) = err.downcast_ref::<T>() {
150 return Some(err);
151 }
152 source = err.source();
153 }
154 None
155}
156
157#[derive(Debug, Clone, thiserror::Error)]
159#[non_exhaustive]
160pub enum AppendConditionFailed {
161 #[error("fencing token mismatch, expected: {0}")]
163 FencingTokenMismatch(FencingToken),
164 #[error("sequence number mismatch, expected: {0}")]
166 SeqNumMismatch(u64),
167}
168
169impl From<api::stream::AppendConditionFailed> for AppendConditionFailed {
170 fn from(value: api::stream::AppendConditionFailed) -> Self {
171 match value {
172 api::stream::AppendConditionFailed::FencingTokenMismatch(token) => {
173 Self::FencingTokenMismatch(FencingToken::from_server(token.to_string()))
174 }
175 api::stream::AppendConditionFailed::SeqNumMismatch(seq) => Self::SeqNumMismatch(seq),
176 }
177 }
178}
179
180#[derive(Debug, Clone, thiserror::Error)]
182#[non_exhaustive]
183pub enum RequestError {
184 #[error(transparent)]
186 Client(#[from] ClientError),
187 #[error(transparent)]
189 Server(#[from] ServerError),
190 #[error("malformed access token: {0}")]
192 MalformedAccessToken(String),
193 #[cfg(feature = "_hidden")]
194 #[doc(hidden)]
195 #[error("access token provider failed: {0}")]
196 AccessTokenProvider(crate::types::AccessTokenProviderError),
197 #[error(transparent)]
199 Validation(#[from] ValidationError),
200}
201
202impl RequestError {
203 pub fn is_retryable(&self) -> bool {
205 match self {
206 Self::Client(error) => error.is_retryable(),
207 Self::Server(error) => error.is_retryable(),
208 #[cfg(feature = "_hidden")]
209 Self::AccessTokenProvider(error) => error.is_retryable(),
210 Self::MalformedAccessToken(_) | Self::Validation(_) => false,
211 }
212 }
213
214 pub fn has_no_side_effects(&self) -> bool {
216 match self {
217 Self::Client(error) => error.has_no_side_effects(),
218 Self::Server(error) => error.has_no_side_effects(),
219 #[cfg(feature = "_hidden")]
220 Self::AccessTokenProvider(_) => true,
221 Self::MalformedAccessToken(_) | Self::Validation(_) => true,
222 }
223 }
224
225 pub fn server_error(&self) -> Option<&ServerError> {
227 match self {
228 Self::Server(error) => Some(error),
229 _ => None,
230 }
231 }
232
233 pub(crate) fn is_authentication_error(&self) -> bool {
234 matches!(
235 self,
236 Self::Server(error)
237 if error.status == StatusCode::UNAUTHORIZED && error.code == "authn"
238 )
239 }
240}
241
242impl From<ApiError> for RequestError {
243 fn from(error: ApiError) -> Self {
244 match error {
245 ApiError::Client(error) => Self::Client(error),
246 ApiError::ProtoDecode(error) => {
247 Self::Client(ClientError::ResponseDecode(error.to_string()))
248 }
249 ApiError::TerminalDecode(error) => {
250 Self::Client(ClientError::SessionProtocol(error.to_string()))
251 }
252 ApiError::MalformedAccessToken(error) => Self::MalformedAccessToken(error),
253 #[cfg(feature = "_hidden")]
254 ApiError::AccessTokenProvider(error) => Self::AccessTokenProvider(error),
255 ApiError::Compression(error) => {
256 Self::Client(ClientError::ResponseCompression(error.to_string()))
257 }
258 ApiError::Server(status, response) => {
259 Self::Server(ServerError::from_api(status, response))
260 }
261 other => Self::Client(ClientError::Other(other.to_string())),
262 }
263 }
264}
265
266#[derive(Debug, Clone, thiserror::Error)]
268#[non_exhaustive]
269pub enum ReadError {
270 #[error(transparent)]
272 Request(#[from] RequestError),
273 #[error("read from an unwritten position. current tail: {0}")]
275 ReadUnwritten(StreamPosition),
276}
277
278impl ReadError {
279 pub fn is_retryable(&self) -> bool {
281 matches!(self, Self::Request(error) if error.is_retryable())
282 }
283
284 pub fn request_error(&self) -> Option<&RequestError> {
286 match self {
287 Self::Request(error) => Some(error),
288 Self::ReadUnwritten(_) => None,
289 }
290 }
291}
292
293impl From<ApiError> for ReadError {
294 fn from(error: ApiError) -> Self {
295 match error {
296 ApiError::ReadUnwritten(tail) => Self::ReadUnwritten(tail.tail.into()),
297 other => Self::Request(other.into()),
298 }
299 }
300}
301
302#[derive(Debug, Clone, thiserror::Error)]
304#[non_exhaustive]
305pub enum AppendError {
306 #[error(transparent)]
308 Request(#[from] RequestError),
309 #[error(transparent)]
311 ConditionFailed(#[from] AppendConditionFailed),
312}
313
314impl AppendError {
315 pub fn is_retryable(&self) -> bool {
317 matches!(self, Self::Request(error) if error.is_retryable())
318 }
319
320 pub fn has_no_side_effects(&self) -> bool {
322 match self {
323 Self::Request(error) => error.has_no_side_effects(),
324 Self::ConditionFailed(_) => true,
325 }
326 }
327
328 pub fn request_error(&self) -> Option<&RequestError> {
330 match self {
331 Self::Request(error) => Some(error),
332 Self::ConditionFailed(_) => None,
333 }
334 }
335}
336
337impl From<ApiError> for AppendError {
338 fn from(error: ApiError) -> Self {
339 match error {
340 ApiError::AppendConditionFailed(condition) => Self::ConditionFailed(condition.into()),
341 other => Self::Request(other.into()),
342 }
343 }
344}
345
346#[derive(Debug, Clone, thiserror::Error)]
348#[non_exhaustive]
349pub enum ProducerError {
350 #[error(transparent)]
352 Append(#[from] AppendSessionError),
353 #[error(transparent)]
355 Validation(#[from] ValidationError),
356 #[error("producer already closed")]
358 ProducerClosed,
359 #[error("producer is closing")]
361 ProducerClosing,
362 #[error("producer dropped without calling close")]
364 ProducerDropped,
365}
366
367impl ProducerError {
368 pub fn is_retryable(&self) -> bool {
370 match self {
371 Self::Append(error) => error.is_retryable(),
372 Self::Validation(_)
373 | Self::ProducerClosed
374 | Self::ProducerClosing
375 | Self::ProducerDropped => false,
376 }
377 }
378
379 pub fn has_no_side_effects(&self) -> bool {
381 match self {
382 Self::Append(error) => error.has_no_side_effects(),
383 Self::Validation(_) | Self::ProducerClosed | Self::ProducerClosing => true,
384 Self::ProducerDropped => false,
385 }
386 }
387
388 pub fn request_error(&self) -> Option<&RequestError> {
390 match self {
391 Self::Append(error) => error.request_error(),
392 Self::Validation(_)
393 | Self::ProducerClosed
394 | Self::ProducerClosing
395 | Self::ProducerDropped => None,
396 }
397 }
398}
399
400#[derive(Debug, Clone, thiserror::Error)]
402#[error("{code}: {message}")]
403#[non_exhaustive]
404pub struct ServerError {
405 pub status: StatusCode,
407 pub code: String,
409 pub message: String,
411}
412
413impl ServerError {
414 pub(crate) fn from_api(status: StatusCode, response: ServerErrorBody) -> Self {
415 Self {
416 status,
417 code: response.code,
418 message: response.message,
419 }
420 }
421
422 pub fn known_code(&self) -> Option<ErrorCode> {
427 self.code.parse().ok()
428 }
429
430 pub fn is_retryable(&self) -> bool {
432 server_error_is_retryable(self.status, &self.code)
433 }
434
435 pub fn has_no_side_effects(&self) -> bool {
437 server_error_has_no_side_effects(self.status, &self.code)
438 }
439}
440
441pub(crate) fn server_error_is_retryable(status: StatusCode, code: &str) -> bool {
442 match code.parse::<ErrorCode>() {
443 Ok(code) if code.status() == status => code.is_retryable(),
444 Ok(_) => false,
445 Err(_) => matches!(
446 status,
447 StatusCode::REQUEST_TIMEOUT
448 | StatusCode::TOO_MANY_REQUESTS
449 | StatusCode::INTERNAL_SERVER_ERROR
450 | StatusCode::BAD_GATEWAY
451 | StatusCode::SERVICE_UNAVAILABLE
452 | StatusCode::GATEWAY_TIMEOUT
453 ),
454 }
455}
456
457pub(crate) fn server_error_has_no_side_effects(status: StatusCode, code: &str) -> bool {
458 code.parse::<ErrorCode>()
459 .is_ok_and(|code| code.status() == status && code.has_no_side_effects())
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn response(status: StatusCode, code: &str) -> ServerError {
467 ServerError::from_api(
468 status,
469 ServerErrorBody {
470 code: code.to_owned(),
471 message: "test".to_owned(),
472 },
473 )
474 }
475
476 #[test]
477 fn error_response_preserves_raw_and_known_codes() {
478 let known = response(StatusCode::NOT_FOUND, "basin_not_found");
479 assert_eq!(known.code, "basin_not_found");
480 assert_eq!(known.message, "test");
481 assert_eq!(known.known_code(), Some(ErrorCode::BasinNotFound));
482 assert!(known.to_string().contains("basin_not_found"));
483
484 let unknown = response(StatusCode::BAD_REQUEST, "introduced_by_a_newer_server");
485 assert_eq!(unknown.known_code(), None);
486 assert_eq!(unknown.code, "introduced_by_a_newer_server");
487 }
488
489 #[test]
490 fn server_classification_fails_closed_on_status_mismatch() {
491 let mismatch = response(StatusCode::INTERNAL_SERVER_ERROR, "rate_limited");
492 assert!(!mismatch.is_retryable());
493 assert!(!mismatch.has_no_side_effects());
494 }
495
496 #[test]
497 fn unknown_codes_retain_retryable_status_fallback() {
498 let unknown = response(StatusCode::SERVICE_UNAVAILABLE, "future_server_error");
499 assert!(unknown.is_retryable());
500 assert!(!unknown.has_no_side_effects());
501 }
502
503 #[test]
504 fn internal_client_errors_preserve_the_failure_stage() {
505 assert!(matches!(
506 ClientError::from(client::HttpError::RequestBuild("bad request".to_owned())),
507 ClientError::RequestBuild(message) if message == "bad request"
508 ));
509 assert!(matches!(
510 ClientError::from(client::HttpError::RequestCompression("encode".to_owned())),
511 ClientError::RequestCompression(message) if message == "encode"
512 ));
513 assert!(matches!(
514 ClientError::from(client::HttpError::ResponseCompression("decode".to_owned())),
515 ClientError::ResponseCompression(message) if message == "decode"
516 ));
517
518 let json_error = serde_json::from_slice::<serde_json::Value>(b"{")
519 .expect_err("invalid JSON should fail");
520 assert!(matches!(
521 ClientError::from(client::HttpError::ResponseDecode(json_error)),
522 ClientError::ResponseDecode(_)
523 ));
524 }
525
526 #[test]
527 fn nested_errors_expose_request_and_server_errors() {
528 let append = AppendError::Request(RequestError::Server(response(
529 StatusCode::CONFLICT,
530 "transaction_conflict",
531 )));
532
533 assert!(append.is_retryable());
534 assert!(append.has_no_side_effects());
535 let request = append.request_error().expect("request error");
536 assert!(matches!(request, RequestError::Server(_)));
537 let server = request.server_error().expect("server error");
538 assert_eq!(server.known_code(), Some(ErrorCode::TransactionConflict));
539 }
540}