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 NotImplemented(String),
48
49 #[error("Configuration error: {0}")]
50 Config(String),
51
52 #[error("Database error: {0}")]
53 Database(#[from] DatabaseError),
54
55 #[error("Serialization error: {0}")]
56 Serialization(#[from] serde_json::Error),
57
58 #[error("Plugin error: {plugin} - {message}")]
59 Plugin { plugin: String, message: String },
60
61 #[error("Internal server error: {0}")]
62 Internal(String),
63
64 #[error("Password hashing error: {0}")]
65 PasswordHash(String),
66
67 #[error("JWT error: {0}")]
68 Jwt(#[from] jsonwebtoken::errors::Error),
69}
70
71impl AuthError {
72 pub fn status_code(&self) -> u16 {
74 match self {
75 Self::BadRequest(_) | Self::InvalidRequest(_) | Self::Validation(_) => 400,
77 Self::InvalidCredentials | Self::Unauthenticated | Self::SessionNotFound => 401,
79 Self::Forbidden(_) | Self::Unauthorized => 403,
81 Self::UserNotFound | Self::NotFound(_) => 404,
83 Self::Conflict(_) => 409,
85 Self::RateLimited => 429,
87 Self::NotImplemented(_) => 501,
89 Self::Config(_)
91 | Self::Database(_)
92 | Self::Serialization(_)
93 | Self::Plugin { .. }
94 | Self::Internal(_)
95 | Self::PasswordHash(_)
96 | Self::Jwt(_) => 500,
97 }
98 }
99
100 pub fn into_response(self) -> crate::types::AuthResponse {
105 let status = self.status_code();
106 let message = match status {
107 500 => {
108 tracing::error!(error = %self, "Internal server error");
109 "Internal server error".to_string()
110 }
111 _ => self.to_string(),
112 };
113
114 crate::types::AuthResponse::json(
115 status,
116 &crate::types::ErrorMessageResponse {
117 message: message.clone(),
118 },
119 )
120 .unwrap_or_else(|_| crate::types::AuthResponse::text(status, &message))
121 }
122
123 pub fn bad_request(message: impl Into<String>) -> Self {
124 Self::BadRequest(message.into())
125 }
126
127 pub fn forbidden(message: impl Into<String>) -> Self {
128 Self::Forbidden(message.into())
129 }
130
131 pub fn not_found(message: impl Into<String>) -> Self {
132 Self::NotFound(message.into())
133 }
134
135 pub fn conflict(message: impl Into<String>) -> Self {
136 Self::Conflict(message.into())
137 }
138
139 pub fn not_implemented(message: impl Into<String>) -> Self {
140 Self::NotImplemented(message.into())
141 }
142
143 pub fn plugin(plugin: &str, message: impl Into<String>) -> Self {
144 Self::Plugin {
145 plugin: plugin.to_string(),
146 message: message.into(),
147 }
148 }
149
150 pub fn config(message: impl Into<String>) -> Self {
151 Self::Config(message.into())
152 }
153
154 pub fn internal(message: impl Into<String>) -> Self {
155 Self::Internal(message.into())
156 }
157
158 pub fn validation(message: impl Into<String>) -> Self {
159 Self::Validation(message.into())
160 }
161}
162
163#[derive(Error, Debug)]
164pub enum DatabaseError {
165 #[error("Connection error: {0}")]
166 Connection(String),
167
168 #[error("Query error: {0}")]
169 Query(String),
170
171 #[error("Migration error: {0}")]
172 Migration(String),
173
174 #[error("Constraint violation: {0}")]
175 Constraint(String),
176
177 #[error("Transaction error: {0}")]
178 Transaction(String),
179}
180
181#[cfg(feature = "sqlx-postgres")]
182impl From<sqlx::Error> for DatabaseError {
183 fn from(err: sqlx::Error) -> Self {
184 match err {
185 sqlx::Error::Database(db_err) => {
186 if db_err.is_unique_violation() {
187 DatabaseError::Constraint(db_err.to_string())
188 } else {
189 DatabaseError::Query(db_err.to_string())
190 }
191 }
192 sqlx::Error::PoolClosed => DatabaseError::Connection("Pool closed".to_string()),
193 sqlx::Error::PoolTimedOut => DatabaseError::Connection("Pool timed out".to_string()),
194 _ => DatabaseError::Query(err.to_string()),
195 }
196 }
197}
198
199#[cfg(feature = "sqlx-postgres")]
200impl From<sqlx::Error> for AuthError {
201 fn from(err: sqlx::Error) -> Self {
202 AuthError::Database(DatabaseError::from(err))
203 }
204}
205
206pub type AuthResult<T> = Result<T, AuthError>;
207
208#[cfg(feature = "axum")]
209impl axum::response::IntoResponse for AuthError {
210 fn into_response(self) -> axum::response::Response {
211 let status = axum::http::StatusCode::from_u16(self.status_code())
212 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
213 let message = match self.status_code() {
214 500 => {
215 tracing::error!(error = %self, "Internal server error");
216 "Internal server error".to_string()
217 }
218 _ => self.to_string(),
219 };
220 (
221 status,
222 axum::Json(crate::types::ErrorMessageResponse { message }),
223 )
224 .into_response()
225 }
226}
227
228pub fn validation_error_response(
232 errors: &validator::ValidationErrors,
233) -> crate::types::AuthResponse {
234 let field_errors: std::collections::HashMap<&str, Vec<String>> = errors
235 .field_errors()
236 .into_iter()
237 .map(|(field, errs)| {
238 let messages: Vec<String> = errs
239 .iter()
240 .map(|e| {
241 e.message
242 .as_ref()
243 .map(|m| m.to_string())
244 .unwrap_or_else(|| format!("Invalid value for {}", field))
245 })
246 .collect();
247 (field, messages)
248 })
249 .collect();
250
251 let body = crate::types::ValidationErrorResponse {
252 code: "VALIDATION_ERROR",
253 message: "Validation failed",
254 errors: field_errors,
255 };
256
257 crate::types::AuthResponse::json(422, &body)
258 .unwrap_or_else(|_| crate::types::AuthResponse::text(422, "Validation failed"))
259}
260
261pub fn validate_request_body<T>(
263 req: &crate::types::AuthRequest,
264) -> Result<T, crate::types::AuthResponse>
265where
266 T: serde::de::DeserializeOwned + validator::Validate,
267{
268 let value: T = req.body_as_json().map_err(|e| {
269 crate::types::AuthResponse::json(
270 400,
271 &crate::types::ErrorMessageResponse {
272 message: format!("Invalid JSON: {}", e),
273 },
274 )
275 .unwrap_or_else(|_| crate::types::AuthResponse::text(400, "Invalid JSON"))
276 })?;
277
278 value
279 .validate()
280 .map_err(|e| validation_error_response(&e))?;
281
282 Ok(value)
283}