Skip to main content

better_auth_core/
error.rs

1use thiserror::Error;
2
3/// Authentication framework error types.
4///
5/// Each variant maps to an HTTP status code via [`AuthError::status_code`].
6/// Use [`AuthError::into_response`] to produce a standardized JSON response
7/// matching the better-auth OpenAPI spec: `{ "message": "..." }`.
8#[derive(Error, Debug)]
9pub enum AuthError {
10    // --- 400 Bad Request ---
11    #[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    // --- 401 Unauthorized ---
21    #[error("Invalid credentials")]
22    InvalidCredentials,
23
24    #[error("Authentication required")]
25    Unauthenticated,
26
27    #[error("Session not found or expired")]
28    SessionNotFound,
29
30    // --- 403 Forbidden ---
31    #[error("{0}")]
32    Forbidden(String),
33
34    #[error("Insufficient permissions")]
35    Unauthorized,
36
37    // --- 404 Not Found ---
38    #[error("User not found")]
39    UserNotFound,
40
41    #[error("{0}")]
42    NotFound(String),
43
44    // --- 409 Conflict ---
45    #[error("{0}")]
46    Conflict(String),
47
48    // --- 429 Too Many Requests ---
49    #[error("Too many requests")]
50    RateLimited,
51
52    // --- 501 Not Implemented ---
53    #[error("{0}")]
54    NotImplemented(String),
55
56    // --- 500 Internal Server Error ---
57    #[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    /// HTTP status code for this error.
81    pub fn status_code(&self) -> u16 {
82        match self {
83            // 400
84            Self::BadRequest(_) | Self::InvalidRequest(_) | Self::Validation(_) => 400,
85            // 401
86            Self::InvalidCredentials | Self::Unauthenticated | Self::SessionNotFound => 401,
87            // 403
88            Self::Forbidden(_) | Self::Unauthorized => 403,
89            // 404
90            Self::UserNotFound | Self::NotFound(_) => 404,
91            // 409
92            Self::Conflict(_) => 409,
93            // 429
94            Self::RateLimited => 429,
95            // 501
96            Self::NotImplemented(_) => 501,
97            // 500
98            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    /// Convert this error into a standardized [`AuthResponse`] matching the
109    /// better-auth OpenAPI spec: `{ "message": "..." }`.
110    ///
111    /// Internal errors (500) use a generic message to avoid leaking details.
112    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    // --- Constructors ---
129
130    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
215/// Convert `validator::ValidationErrors` into a standardized error response body.
216///
217/// Returns a 422 response with `{ "code": "VALIDATION_ERROR", "message": "...", "errors": {...} }`.
218pub 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
248/// Validate a request body, returning a parsed + validated value or an error response.
249pub 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}