better_auth_core/
error.rs1use thiserror::Error;
2
3#[derive(Error, Debug)]
9pub enum AuthError {
10 #[error("{0}")]
11 BadRequest(String),
12
13 #[error("Invalid request: {0}")]
14 InvalidRequest(String),
15
16 #[error("Validation error: {0}")]
17 Validation(String),
18
19 #[error("Invalid credentials")]
20 InvalidCredentials,
21
22 #[error("Authentication required")]
23 Unauthenticated,
24
25 #[error("Session not found or expired")]
26 SessionNotFound,
27
28 #[error("{0}")]
29 Forbidden(String),
30
31 #[error("Insufficient permissions")]
32 Unauthorized,
33
34 #[error("User not found")]
35 UserNotFound,
36
37 #[error("{0}")]
38 NotFound(String),
39
40 #[error("{0}")]
41 Conflict(String),
42
43 #[error("Too many requests")]
44 RateLimited,
45
46 #[error("{0}")]
47 PayloadTooLarge(String),
48
49 #[error("{0}")]
50 NotImplemented(String),
51
52 #[error("Configuration error: {0}")]
53 Config(String),
54
55 #[error("Database error: {0}")]
56 Database(#[from] DatabaseError),
57
58 #[error("Serialization error: {0}")]
59 Serialization(#[from] serde_json::Error),
60
61 #[error("Plugin error: {plugin} - {message}")]
62 Plugin { plugin: String, message: String },
63
64 #[error("Internal server error: {0}")]
65 Internal(String),
66
67 #[error("Password hashing error: {0}")]
68 PasswordHash(String),
69
70 #[error("JWT error: {0}")]
71 Jwt(#[from] jsonwebtoken::errors::Error),
72}
73
74impl AuthError {
75 pub fn status_code(&self) -> u16 {
77 match self {
78 Self::BadRequest(_) | Self::InvalidRequest(_) | Self::Validation(_) => 400,
80 Self::InvalidCredentials | Self::Unauthenticated | Self::SessionNotFound => 401,
82 Self::Forbidden(_) | Self::Unauthorized => 403,
84 Self::UserNotFound | Self::NotFound(_) => 404,
86 Self::Conflict(_) => 409,
88 Self::PayloadTooLarge(_) => 413,
90 Self::RateLimited => 429,
92 Self::NotImplemented(_) => 501,
94 Self::Config(_)
96 | Self::Database(_)
97 | Self::Serialization(_)
98 | Self::Plugin { .. }
99 | Self::Internal(_)
100 | Self::PasswordHash(_)
101 | Self::Jwt(_) => 500,
102 }
103 }
104
105 pub fn into_response(self) -> crate::types::AuthResponse {
110 let status = self.status_code();
111 let message = match status {
112 500 => {
113 tracing::error!(error = %self, "Internal server error");
114 "Internal server error".to_string()
115 }
116 _ => self.to_string(),
117 };
118
119 crate::types::AuthResponse::json(
120 status,
121 &crate::types::ErrorMessageResponse {
122 message: message.clone(),
123 },
124 )
125 .unwrap_or_else(|_| crate::types::AuthResponse::text(status, &message))
126 }
127
128 pub fn bad_request(message: impl Into<String>) -> Self {
129 Self::BadRequest(message.into())
130 }
131
132 pub fn forbidden(message: impl Into<String>) -> Self {
133 Self::Forbidden(message.into())
134 }
135
136 pub fn not_found(message: impl Into<String>) -> Self {
137 Self::NotFound(message.into())
138 }
139
140 pub fn conflict(message: impl Into<String>) -> Self {
141 Self::Conflict(message.into())
142 }
143
144 pub fn not_implemented(message: impl Into<String>) -> Self {
145 Self::NotImplemented(message.into())
146 }
147
148 pub fn payload_too_large(message: impl Into<String>) -> Self {
149 Self::PayloadTooLarge(message.into())
150 }
151
152 pub fn plugin(plugin: &str, message: impl Into<String>) -> Self {
153 Self::Plugin {
154 plugin: plugin.to_string(),
155 message: message.into(),
156 }
157 }
158
159 pub fn config(message: impl Into<String>) -> Self {
160 Self::Config(message.into())
161 }
162
163 pub fn internal(message: impl Into<String>) -> Self {
164 Self::Internal(message.into())
165 }
166
167 pub fn validation(message: impl Into<String>) -> Self {
168 Self::Validation(message.into())
169 }
170}
171
172#[derive(Error, Debug)]
173pub enum DatabaseError {
174 #[error("Connection error: {0}")]
175 Connection(String),
176
177 #[error("Query error: {0}")]
178 Query(String),
179
180 #[error("Migration error: {0}")]
181 Migration(String),
182
183 #[error("Constraint violation: {0}")]
184 Constraint(String),
185
186 #[error("Transaction error: {0}")]
187 Transaction(String),
188}
189
190#[cfg(feature = "sqlx-postgres")]
191impl From<sqlx::Error> for DatabaseError {
192 fn from(err: sqlx::Error) -> Self {
193 match err {
194 sqlx::Error::Database(db_err) => {
195 if db_err.is_unique_violation() {
196 DatabaseError::Constraint(db_err.to_string())
197 } else {
198 DatabaseError::Query(db_err.to_string())
199 }
200 }
201 sqlx::Error::PoolClosed => DatabaseError::Connection("Pool closed".to_string()),
202 sqlx::Error::PoolTimedOut => DatabaseError::Connection("Pool timed out".to_string()),
203 _ => DatabaseError::Query(err.to_string()),
204 }
205 }
206}
207
208#[cfg(feature = "sqlx-postgres")]
209impl From<sqlx::Error> for AuthError {
210 fn from(err: sqlx::Error) -> Self {
211 AuthError::Database(DatabaseError::from(err))
212 }
213}
214
215pub type AuthResult<T> = Result<T, AuthError>;
216
217#[cfg(feature = "axum")]
218impl axum::response::IntoResponse for AuthError {
219 fn into_response(self) -> axum::response::Response {
220 let status = axum::http::StatusCode::from_u16(self.status_code())
221 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
222 let message = match self.status_code() {
223 500 => {
224 tracing::error!(error = %self, "Internal server error");
225 "Internal server error".to_string()
226 }
227 _ => self.to_string(),
228 };
229 (
230 status,
231 axum::Json(crate::types::ErrorMessageResponse { message }),
232 )
233 .into_response()
234 }
235}
236
237pub fn validation_error_response(
241 errors: &validator::ValidationErrors,
242) -> crate::types::AuthResponse {
243 let field_errors: std::collections::HashMap<&str, Vec<String>> = errors
244 .field_errors()
245 .into_iter()
246 .map(|(field, errs)| {
247 let messages: Vec<String> = errs
248 .iter()
249 .map(|e| {
250 e.message
251 .as_ref()
252 .map(|m| m.to_string())
253 .unwrap_or_else(|| format!("Invalid value for {}", field))
254 })
255 .collect();
256 (field, messages)
257 })
258 .collect();
259
260 let body = crate::types::ValidationErrorResponse {
261 code: "VALIDATION_ERROR",
262 message: "Validation failed",
263 errors: field_errors,
264 };
265
266 crate::types::AuthResponse::json(422, &body)
267 .unwrap_or_else(|_| crate::types::AuthResponse::text(422, "Validation failed"))
268}
269
270pub fn validate_request_body<T>(
272 req: &crate::types::AuthRequest,
273) -> Result<T, crate::types::AuthResponse>
274where
275 T: serde::de::DeserializeOwned + validator::Validate,
276{
277 let value: T = req.body_as_json().map_err(|e| {
278 crate::types::AuthResponse::json(
279 400,
280 &crate::types::ErrorMessageResponse {
281 message: format!("Invalid JSON: {}", e),
282 },
283 )
284 .unwrap_or_else(|_| crate::types::AuthResponse::text(400, "Invalid JSON"))
285 })?;
286
287 value
288 .validate()
289 .map_err(|e| validation_error_response(&e))?;
290
291 Ok(value)
292}