durable_streams_server/protocol/
error.rs1use thiserror::Error;
2
3#[derive(Debug, Error)]
8pub enum Error {
9 #[error("Stream not found: {0}")]
11 NotFound(String),
12
13 #[error("Stream already exists with different configuration")]
15 ConfigMismatch,
16
17 #[error("Stream already exists: {0}")]
19 AlreadyExists(String),
20
21 #[error("Invalid offset format: {0}")]
23 InvalidOffset(String),
24
25 #[error("Invalid stream name: {0}")]
27 InvalidStreamName(String),
28
29 #[error("Content type mismatch: expected {expected}, got {actual}")]
31 ContentTypeMismatch { expected: String, actual: String },
32
33 #[error("Stream is closed")]
35 StreamClosed,
36
37 #[error("Producer sequence regression: expected > {expected}, got {actual}")]
39 SequenceRegression { expected: u64, actual: u64 },
40
41 #[error("Producer sequence gap: expected {expected}, got {actual}")]
43 SequenceGap { expected: u64, actual: u64 },
44
45 #[error("Producer epoch fenced: current {current}, received {received}")]
47 EpochFenced { current: u64, received: u64 },
48
49 #[error("Invalid producer state: {0}")]
51 InvalidProducerState(String),
52
53 #[error("Memory limit exceeded")]
55 MemoryLimitExceeded,
56
57 #[error("Stream size limit exceeded")]
59 StreamSizeLimitExceeded,
60
61 #[error("Invalid TTL format: {0}")]
63 InvalidTtl(String),
64
65 #[error("Cannot specify both TTL and Expires-At")]
67 ConflictingExpiration,
68
69 #[error("Stream has expired")]
71 StreamExpired,
72
73 #[error("Invalid JSON: {0}")]
75 InvalidJson(String),
76
77 #[error("Empty request body")]
79 EmptyBody,
80
81 #[error("Invalid header value for {header}: {reason}")]
83 InvalidHeader { header: String, reason: String },
84
85 #[error("Stream-Seq ordering violation: last={last}, received={received}")]
87 SeqOrderingViolation { last: String, received: String },
88
89 #[error("Storage error: {0}")]
91 Storage(String),
92}
93
94impl Error {
95 #[must_use]
100 pub fn status_code(&self) -> u16 {
101 match self {
102 Self::NotFound(_) | Self::StreamExpired => 404,
103 Self::ConfigMismatch
104 | Self::ContentTypeMismatch { .. }
105 | Self::StreamClosed
106 | Self::SequenceRegression { .. }
107 | Self::SequenceGap { .. }
108 | Self::SeqOrderingViolation { .. } => 409,
109 Self::EpochFenced { .. } => 403,
110 Self::MemoryLimitExceeded | Self::StreamSizeLimitExceeded => 413,
111 Self::AlreadyExists(_)
112 | Self::InvalidOffset(_)
113 | Self::InvalidStreamName(_)
114 | Self::InvalidProducerState(_)
115 | Self::InvalidTtl(_)
116 | Self::ConflictingExpiration
117 | Self::InvalidJson(_)
118 | Self::EmptyBody
119 | Self::InvalidHeader { .. } => 400,
120 Self::Storage(_) => 500,
121 }
122 }
123}
124
125pub type Result<T> = std::result::Result<T, Error>;
127
128impl axum::response::IntoResponse for Error {
130 fn into_response(self) -> axum::response::Response {
131 let status = axum::http::StatusCode::from_u16(self.status_code())
132 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
133
134 let body = self.to_string();
135
136 (status, body).into_response()
137 }
138}