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