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 #[error(transparent)]
195 Validation(#[from] ValidationError),
196}
197
198impl RequestError {
199 pub fn is_retryable(&self) -> bool {
201 match self {
202 Self::Client(error) => error.is_retryable(),
203 Self::Server(error) => error.is_retryable(),
204 Self::MalformedAccessToken(_) | Self::Validation(_) => false,
205 }
206 }
207
208 pub fn has_no_side_effects(&self) -> bool {
210 match self {
211 Self::Client(error) => error.has_no_side_effects(),
212 Self::Server(error) => error.has_no_side_effects(),
213 Self::MalformedAccessToken(_) | Self::Validation(_) => true,
214 }
215 }
216
217 pub fn server_error(&self) -> Option<&ServerError> {
219 match self {
220 Self::Server(error) => Some(error),
221 _ => None,
222 }
223 }
224}
225
226impl From<ApiError> for RequestError {
227 fn from(error: ApiError) -> Self {
228 match error {
229 ApiError::Client(error) => Self::Client(error),
230 ApiError::ProtoDecode(error) => {
231 Self::Client(ClientError::ResponseDecode(error.to_string()))
232 }
233 ApiError::TerminalDecode(error) => {
234 Self::Client(ClientError::SessionProtocol(error.to_string()))
235 }
236 ApiError::MalformedAccessToken(error) => Self::MalformedAccessToken(error),
237 ApiError::Compression(error) => {
238 Self::Client(ClientError::ResponseCompression(error.to_string()))
239 }
240 ApiError::Server(status, response) => {
241 Self::Server(ServerError::from_api(status, response))
242 }
243 other => Self::Client(ClientError::Other(other.to_string())),
244 }
245 }
246}
247
248#[derive(Debug, Clone, thiserror::Error)]
250#[non_exhaustive]
251pub enum ReadError {
252 #[error(transparent)]
254 Request(#[from] RequestError),
255 #[error("read from an unwritten position. current tail: {0}")]
257 ReadUnwritten(StreamPosition),
258}
259
260impl ReadError {
261 pub fn is_retryable(&self) -> bool {
263 matches!(self, Self::Request(error) if error.is_retryable())
264 }
265
266 pub fn request_error(&self) -> Option<&RequestError> {
268 match self {
269 Self::Request(error) => Some(error),
270 Self::ReadUnwritten(_) => None,
271 }
272 }
273}
274
275impl From<ApiError> for ReadError {
276 fn from(error: ApiError) -> Self {
277 match error {
278 ApiError::ReadUnwritten(tail) => Self::ReadUnwritten(tail.tail.into()),
279 other => Self::Request(other.into()),
280 }
281 }
282}
283
284#[derive(Debug, Clone, thiserror::Error)]
286#[non_exhaustive]
287pub enum AppendError {
288 #[error(transparent)]
290 Request(#[from] RequestError),
291 #[error(transparent)]
293 ConditionFailed(#[from] AppendConditionFailed),
294}
295
296impl AppendError {
297 pub fn is_retryable(&self) -> bool {
299 matches!(self, Self::Request(error) if error.is_retryable())
300 }
301
302 pub fn has_no_side_effects(&self) -> bool {
304 match self {
305 Self::Request(error) => error.has_no_side_effects(),
306 Self::ConditionFailed(_) => true,
307 }
308 }
309
310 pub fn request_error(&self) -> Option<&RequestError> {
312 match self {
313 Self::Request(error) => Some(error),
314 Self::ConditionFailed(_) => None,
315 }
316 }
317}
318
319impl From<ApiError> for AppendError {
320 fn from(error: ApiError) -> Self {
321 match error {
322 ApiError::AppendConditionFailed(condition) => Self::ConditionFailed(condition.into()),
323 other => Self::Request(other.into()),
324 }
325 }
326}
327
328#[derive(Debug, Clone, thiserror::Error)]
330#[non_exhaustive]
331pub enum ProducerError {
332 #[error(transparent)]
334 Append(#[from] AppendSessionError),
335 #[error(transparent)]
337 Validation(#[from] ValidationError),
338 #[error("producer already closed")]
340 ProducerClosed,
341 #[error("producer is closing")]
343 ProducerClosing,
344 #[error("producer dropped without calling close")]
346 ProducerDropped,
347}
348
349impl ProducerError {
350 pub fn is_retryable(&self) -> bool {
352 match self {
353 Self::Append(error) => error.is_retryable(),
354 Self::Validation(_)
355 | Self::ProducerClosed
356 | Self::ProducerClosing
357 | Self::ProducerDropped => false,
358 }
359 }
360
361 pub fn has_no_side_effects(&self) -> bool {
363 match self {
364 Self::Append(error) => error.has_no_side_effects(),
365 Self::Validation(_) | Self::ProducerClosed | Self::ProducerClosing => true,
366 Self::ProducerDropped => false,
367 }
368 }
369
370 pub fn request_error(&self) -> Option<&RequestError> {
372 match self {
373 Self::Append(error) => error.request_error(),
374 Self::Validation(_)
375 | Self::ProducerClosed
376 | Self::ProducerClosing
377 | Self::ProducerDropped => None,
378 }
379 }
380}
381
382#[derive(Debug, Clone, thiserror::Error)]
384#[error("{code}: {message}")]
385#[non_exhaustive]
386pub struct ServerError {
387 pub status: StatusCode,
389 pub code: String,
391 pub message: String,
393}
394
395impl ServerError {
396 pub(crate) fn from_api(status: StatusCode, response: ServerErrorBody) -> Self {
397 Self {
398 status,
399 code: response.code,
400 message: response.message,
401 }
402 }
403
404 pub fn known_code(&self) -> Option<ErrorCode> {
409 self.code.parse().ok()
410 }
411
412 pub fn is_retryable(&self) -> bool {
414 server_error_is_retryable(self.status, &self.code)
415 }
416
417 pub fn has_no_side_effects(&self) -> bool {
419 server_error_has_no_side_effects(self.status, &self.code)
420 }
421}
422
423pub(crate) fn server_error_is_retryable(status: StatusCode, code: &str) -> bool {
424 match code.parse::<ErrorCode>() {
425 Ok(code) if code.status() == status => code.is_retryable(),
426 Ok(_) => false,
427 Err(_) => matches!(
428 status,
429 StatusCode::REQUEST_TIMEOUT
430 | StatusCode::TOO_MANY_REQUESTS
431 | StatusCode::INTERNAL_SERVER_ERROR
432 | StatusCode::BAD_GATEWAY
433 | StatusCode::SERVICE_UNAVAILABLE
434 | StatusCode::GATEWAY_TIMEOUT
435 ),
436 }
437}
438
439pub(crate) fn server_error_has_no_side_effects(status: StatusCode, code: &str) -> bool {
440 code.parse::<ErrorCode>()
441 .is_ok_and(|code| code.status() == status && code.has_no_side_effects())
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn response(status: StatusCode, code: &str) -> ServerError {
449 ServerError::from_api(
450 status,
451 ServerErrorBody {
452 code: code.to_owned(),
453 message: "test".to_owned(),
454 },
455 )
456 }
457
458 #[test]
459 fn error_response_preserves_raw_and_known_codes() {
460 let known = response(StatusCode::NOT_FOUND, "basin_not_found");
461 assert_eq!(known.code, "basin_not_found");
462 assert_eq!(known.message, "test");
463 assert_eq!(known.known_code(), Some(ErrorCode::BasinNotFound));
464 assert!(known.to_string().contains("basin_not_found"));
465
466 let unknown = response(StatusCode::BAD_REQUEST, "introduced_by_a_newer_server");
467 assert_eq!(unknown.known_code(), None);
468 assert_eq!(unknown.code, "introduced_by_a_newer_server");
469 }
470
471 #[test]
472 fn server_classification_fails_closed_on_status_mismatch() {
473 let mismatch = response(StatusCode::INTERNAL_SERVER_ERROR, "rate_limited");
474 assert!(!mismatch.is_retryable());
475 assert!(!mismatch.has_no_side_effects());
476 }
477
478 #[test]
479 fn unknown_codes_retain_retryable_status_fallback() {
480 let unknown = response(StatusCode::SERVICE_UNAVAILABLE, "future_server_error");
481 assert!(unknown.is_retryable());
482 assert!(!unknown.has_no_side_effects());
483 }
484
485 #[test]
486 fn internal_client_errors_preserve_the_failure_stage() {
487 assert!(matches!(
488 ClientError::from(client::HttpError::RequestBuild("bad request".to_owned())),
489 ClientError::RequestBuild(message) if message == "bad request"
490 ));
491 assert!(matches!(
492 ClientError::from(client::HttpError::RequestCompression("encode".to_owned())),
493 ClientError::RequestCompression(message) if message == "encode"
494 ));
495 assert!(matches!(
496 ClientError::from(client::HttpError::ResponseCompression("decode".to_owned())),
497 ClientError::ResponseCompression(message) if message == "decode"
498 ));
499
500 let json_error = serde_json::from_slice::<serde_json::Value>(b"{")
501 .expect_err("invalid JSON should fail");
502 assert!(matches!(
503 ClientError::from(client::HttpError::ResponseDecode(json_error)),
504 ClientError::ResponseDecode(_)
505 ));
506 }
507
508 #[test]
509 fn nested_errors_expose_request_and_server_errors() {
510 let append = AppendError::Request(RequestError::Server(response(
511 StatusCode::CONFLICT,
512 "transaction_conflict",
513 )));
514
515 assert!(append.is_retryable());
516 assert!(append.has_no_side_effects());
517 let request = append.request_error().expect("request error");
518 assert!(matches!(request, RequestError::Server(_)));
519 let server = request.server_error().expect("server error");
520 assert_eq!(server.known_code(), Some(ErrorCode::TransactionConflict));
521 }
522}