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 => "Internal server error".to_string(),
108 _ => self.to_string(),
109 };
110
111 crate::types::AuthResponse::json(
112 status,
113 &crate::types::ErrorMessageResponse {
114 message: message.clone(),
115 },
116 )
117 .unwrap_or_else(|_| crate::types::AuthResponse::text(status, &message))
118 }
119
120 pub fn bad_request(message: impl Into<String>) -> Self {
121 Self::BadRequest(message.into())
122 }
123
124 pub fn forbidden(message: impl Into<String>) -> Self {
125 Self::Forbidden(message.into())
126 }
127
128 pub fn not_found(message: impl Into<String>) -> Self {
129 Self::NotFound(message.into())
130 }
131
132 pub fn conflict(message: impl Into<String>) -> Self {
133 Self::Conflict(message.into())
134 }
135
136 pub fn not_implemented(message: impl Into<String>) -> Self {
137 Self::NotImplemented(message.into())
138 }
139
140 pub fn plugin(plugin: &str, message: impl Into<String>) -> Self {
141 Self::Plugin {
142 plugin: plugin.to_string(),
143 message: message.into(),
144 }
145 }
146
147 pub fn config(message: impl Into<String>) -> Self {
148 Self::Config(message.into())
149 }
150
151 pub fn internal(message: impl Into<String>) -> Self {
152 Self::Internal(message.into())
153 }
154
155 pub fn validation(message: impl Into<String>) -> Self {
156 Self::Validation(message.into())
157 }
158}
159
160#[derive(Error, Debug)]
161pub enum DatabaseError {
162 #[error("Connection error: {0}")]
163 Connection(String),
164
165 #[error("Query error: {0}")]
166 Query(String),
167
168 #[error("Migration error: {0}")]
169 Migration(String),
170
171 #[error("Constraint violation: {0}")]
172 Constraint(String),
173
174 #[error("Transaction error: {0}")]
175 Transaction(String),
176}
177
178#[cfg(feature = "sqlx-postgres")]
179impl From<sqlx::Error> for DatabaseError {
180 fn from(err: sqlx::Error) -> Self {
181 match err {
182 sqlx::Error::Database(db_err) => {
183 if db_err.is_unique_violation() {
184 DatabaseError::Constraint(db_err.to_string())
185 } else {
186 DatabaseError::Query(db_err.to_string())
187 }
188 }
189 sqlx::Error::PoolClosed => DatabaseError::Connection("Pool closed".to_string()),
190 sqlx::Error::PoolTimedOut => DatabaseError::Connection("Pool timed out".to_string()),
191 _ => DatabaseError::Query(err.to_string()),
192 }
193 }
194}
195
196#[cfg(feature = "sqlx-postgres")]
197impl From<sqlx::Error> for AuthError {
198 fn from(err: sqlx::Error) -> Self {
199 AuthError::Database(DatabaseError::from(err))
200 }
201}
202
203pub type AuthResult<T> = Result<T, AuthError>;
204
205pub fn validation_error_response(
209 errors: &validator::ValidationErrors,
210) -> crate::types::AuthResponse {
211 let field_errors: std::collections::HashMap<&str, Vec<String>> = errors
212 .field_errors()
213 .into_iter()
214 .map(|(field, errs)| {
215 let messages: Vec<String> = errs
216 .iter()
217 .map(|e| {
218 e.message
219 .as_ref()
220 .map(|m| m.to_string())
221 .unwrap_or_else(|| format!("Invalid value for {}", field))
222 })
223 .collect();
224 (field, messages)
225 })
226 .collect();
227
228 let body = crate::types::ValidationErrorResponse {
229 code: "VALIDATION_ERROR",
230 message: "Validation failed",
231 errors: field_errors,
232 };
233
234 crate::types::AuthResponse::json(422, &body)
235 .unwrap_or_else(|_| crate::types::AuthResponse::text(422, "Validation failed"))
236}
237
238pub fn validate_request_body<T>(
240 req: &crate::types::AuthRequest,
241) -> Result<T, crate::types::AuthResponse>
242where
243 T: serde::de::DeserializeOwned + validator::Validate,
244{
245 let value: T = req.body_as_json().map_err(|e| {
246 crate::types::AuthResponse::json(
247 400,
248 &crate::types::ErrorMessageResponse {
249 message: format!("Invalid JSON: {}", e),
250 },
251 )
252 .unwrap_or_else(|_| crate::types::AuthResponse::text(400, "Invalid JSON"))
253 })?;
254
255 value
256 .validate()
257 .map_err(|e| validation_error_response(&e))?;
258
259 Ok(value)
260}