Skip to main content

max / makenotwork

Convert .map_err Internal(anyhow!) to ResultExt::context, fix variant misuse Sweep across server/. Most sites now use ResultExt::context (or with_context) instead of the manual .map_err(|e| AppError::Internal(anyhow::anyhow!("X: {}", e)))? form. Same resulting error chain, fewer lines, harder to drift from convention. Variant-misuse fixes (now return ServiceUnavailable instead of Internal, so on-call alerts don't fire when a feature is intentionally disabled on a deployment): - SyncKit storage / JWT secret not configured (lib.rs, synckit_auth.rs, routes/synckit/auth.rs, routes/synckit/blobs.rs, routes/oauth.rs) - Stripe not configured for refund (routes/api/items/refund.rs) - File storage not configured for export and guest download (routes/api/exports/content.rs, routes/api/guest_checkout.rs) - GIT_REPOS_PATH not configured (routes/api/internal/git.rs) Other fixes: - routes/api/users/profile.rs: email send error path now propagates the original AppError instead of re-wrapping as Internal with an uninformative string. - routes/api/promo_codes.rs: NaiveTime::from_hms_opt with literal args is infallible — replace ok_or_else(Internal) with .expect(). - routes/git_issues/push_refs.rs: static regex compiles use .expect() with a reason instead of .unwrap(). - lib.rs: robots.txt static response builder uses .expect() instead of .unwrap(). argon2 / password_hash / totp_rs::TOTP::new errors don't satisfy std::error::Error + Send + Sync + 'static, so those few sites keep the manual map_err form.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-15 03:45 UTC
Commit: d3ff18af95bff4ae62d44a95abef5335a957919d
Parent: 4ea58fd
28 files changed, +118 insertions, -125 deletions
M server/src/auth.rs +12 -12
@@ -32,7 +32,7 @@
32 32 use crate::config::Config;
33 33 use crate::constants;
34 34 use crate::db::{self, UserId, UserSessionId, Username};
35 - use crate::error::AppError;
35 + use crate::error::{AppError, ResultExt};
36 36 use crate::helpers::constant_time_compare;
37 37
38 38 /// Session key for storing user data
@@ -143,7 +143,7 @@
143 143 let user: SessionUser = session
144 144 .get(USER_SESSION_KEY)
145 145 .await
146 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?
146 + .context("session error")?
147 147 .ok_or(AppError::Unauthorized)?;
148 148
149 149 // Validate session tracking (skip for legacy sessions without tracking ID).
@@ -220,7 +220,7 @@
220 220 let user: Option<SessionUser> = session
221 221 .get(USER_SESSION_KEY)
222 222 .await
223 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
223 + .context("session error")?;
224 224
225 225 Ok(MaybeUser(user))
226 226 }
@@ -285,15 +285,15 @@
285 285 let salt = SaltString::generate(&mut OsRng);
286 286 #[cfg(feature = "fast-tests")]
287 287 let params = Params::new(8 * 1024, 1, 1, None)
288 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Argon2 params error: {}", e)))?;
288 + .map_err(|e| AppError::Internal(anyhow::anyhow!("argon2 params: {e}")))?;
289 289 #[cfg(not(feature = "fast-tests"))]
290 290 let params = Params::new(46 * 1024, 2, 1, None)
291 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Argon2 params error: {}", e)))?;
291 + .map_err(|e| AppError::Internal(anyhow::anyhow!("argon2 params: {e}")))?;
292 292 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
293 293
294 294 let hash = argon2
295 295 .hash_password(password.as_bytes(), &salt)
296 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Password hashing failed: {}", e)))?;
296 + .map_err(|e| AppError::Internal(anyhow::anyhow!("password hashing: {e}")))?;
297 297
298 298 Ok(hash.to_string())
299 299 }
@@ -301,7 +301,7 @@
301 301 /// Verify a password against a hash
302 302 pub fn verify_password(password: &str, hash: &str) -> Result<bool, AppError> {
303 303 let parsed_hash = PasswordHash::new(hash)
304 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid password hash: {}", e)))?;
304 + .map_err(|e| AppError::Internal(anyhow::anyhow!("parse password hash: {e}")))?;
305 305
306 306 Ok(Argon2::default()
307 307 .verify_password(password.as_bytes(), &parsed_hash)
@@ -316,19 +316,19 @@
316 316 session
317 317 .cycle_id()
318 318 .await
319 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session cycle failed: {}", e)))?;
319 + .context("session cycle")?;
320 320
321 321 // Regenerate CSRF token so pre-auth tokens can't be used post-auth
322 322 let new_csrf = crate::csrf::generate_token();
323 323 session
324 324 .insert(crate::csrf::CSRF_SESSION_KEY, &new_csrf)
325 325 .await
326 - .map_err(|e| AppError::Internal(anyhow::anyhow!("CSRF token insert failed: {}", e)))?;
326 + .context("csrf token insert")?;
327 327
328 328 session
329 329 .insert(USER_SESSION_KEY, user)
330 330 .await
331 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session insert failed: {}", e)))?;
331 + .context("session insert")?;
332 332 Ok(())
333 333 }
334 334
@@ -339,7 +339,7 @@
339 339 session
340 340 .flush()
341 341 .await
342 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session flush failed: {}", e)))?;
342 + .context("session flush")?;
343 343 Ok(())
344 344 }
345 345
@@ -365,7 +365,7 @@
365 365 session
366 366 .insert(SESSION_TRACKING_KEY, tracking_id)
367 367 .await
368 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session insert failed: {}", e)))?;
368 + .context("session insert")?;
369 369
370 370 Ok(())
371 371 }
@@ -14,7 +14,7 @@
14 14 use rand::RngCore;
15 15 use tower_sessions::Session;
16 16
17 - use crate::error::AppError;
17 + use crate::error::{AppError, ResultExt};
18 18
19 19 /// Session key for storing CSRF token
20 20 pub const CSRF_SESSION_KEY: &str = "csrf_token";
@@ -35,7 +35,7 @@
35 35 if let Some(token) = session
36 36 .get::<String>(CSRF_SESSION_KEY)
37 37 .await
38 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?
38 + .context("session error")?
39 39 {
40 40 return Ok(token);
41 41 }
@@ -45,7 +45,7 @@
45 45 session
46 46 .insert(CSRF_SESSION_KEY, &token)
47 47 .await
48 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session insert error: {}", e)))?;
48 + .context("session insert")?;
49 49
50 50 Ok(token)
51 51 }
@@ -55,7 +55,7 @@
55 55 let session_token: Option<String> = session
56 56 .get(CSRF_SESSION_KEY)
57 57 .await
58 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
58 + .context("session error")?;
59 59
60 60 match session_token {
61 61 Some(token) => Ok(crate::helpers::constant_time_compare(&token, provided_token)),
@@ -106,14 +106,14 @@
106 106 pub fn require_s3(&self) -> error::Result<&Arc<dyn StorageBackend>> {
107 107 self.s3
108 108 .as_ref()
109 - .ok_or_else(|| error::AppError::Storage("File storage is not configured".to_string()))
109 + .ok_or_else(|| error::AppError::ServiceUnavailable("File storage is not configured".to_string()))
110 110 }
111 111
112 112 /// Get the SyncKit S3 storage backend, or error if not configured.
113 113 pub fn require_synckit_s3(&self) -> error::Result<&Arc<dyn StorageBackend>> {
114 114 self.synckit_s3
115 115 .as_ref()
116 - .ok_or_else(|| error::AppError::Internal(anyhow::anyhow!("SyncKit storage not configured")))
116 + .ok_or_else(|| error::AppError::ServiceUnavailable("SyncKit storage is not configured".to_string()))
117 117 }
118 118 }
119 119
@@ -143,7 +143,7 @@
143 143 axum::response::Response::builder()
144 144 .header("content-type", "text/plain")
145 145 .body(axum::body::Body::from("User-agent: *\nDisallow: /api/\nDisallow: /admin/\nDisallow: /settings/\n"))
146 - .unwrap()
146 + .expect("static robots.txt response builds")
147 147 }))
148 148 .nest_service(
149 149 "/static",
@@ -9,7 +9,7 @@
9 9
10 10 use crate::config::StorageConfig;
11 11 use crate::db::{ItemId, ProjectId, UserId};
12 - use crate::error::{AppError, Result};
12 + use crate::error::{AppError, Result, ResultExt};
13 13
14 14 /// Allowed audio file extensions and their MIME types
15 15 const ALLOWED_AUDIO_TYPES: &[(&str, &str)] = &[
@@ -184,7 +184,7 @@
184 184 async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
185 185 let data = tokio::fs::read(file_path)
186 186 .await
187 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to read file: {e}")))?;
187 + .context("read multipart upload source file")?;
188 188 self.upload_object(s3_key, content_type, data, None).await
189 189 }
190 190 async fn check_connectivity(&self) -> std::result::Result<(), String>;
@@ -8,7 +8,7 @@
8 8
9 9 use crate::constants::SYNCKIT_JWT_EXPIRY_SECS;
10 10 use crate::db::{SyncAppId, UserId};
11 - use crate::error::AppError;
11 + use crate::error::{AppError, ResultExt};
12 12 use crate::AppState;
13 13
14 14 /// Issuer claim value for all SyncKit JWTs.
@@ -45,7 +45,7 @@
45 45 &claims,
46 46 &EncodingKey::from_secret(secret.as_bytes()),
47 47 )
48 - .map_err(|e| AppError::Internal(anyhow::anyhow!("JWT encode error: {}", e)))?;
48 + .context("jwt encode")?;
49 49
50 50 Ok(token)
51 51 }
@@ -85,7 +85,7 @@
85 85 .synckit_jwt_secret
86 86 .as_deref()
87 87 .ok_or_else(|| {
88 - AppError::Internal(anyhow::anyhow!("SyncKit not configured"))
88 + AppError::ServiceUnavailable("SyncKit is not configured".to_string())
89 89 })?;
90 90
91 91 let auth_header = parts
@@ -102,8 +102,7 @@
102 102
103 103 // Verify the app is still active (JWT may outlive app deactivation)
104 104 let app = crate::db::synckit::get_sync_app_by_id(&state.db, claims.app)
105 - .await
106 - .map_err(|_| AppError::Internal(anyhow::anyhow!("Failed to verify sync app")))?
105 + .await?
107 106 .ok_or(AppError::Unauthorized)?;
108 107 if !app.is_active {
109 108 return Err(AppError::Unauthorized);
@@ -111,8 +110,7 @@
111 110
112 111 // Verify user is not suspended or deactivated (JWT may outlive suspension)
113 112 let user = crate::db::users::get_user_by_id(&state.db, claims.sub)
114 - .await
115 - .map_err(|_| AppError::Internal(anyhow::anyhow!("Failed to verify sync user")))?
113 + .await?
116 114 .ok_or(AppError::Unauthorized)?;
117 115 if user.is_suspended() || user.is_deactivated() {
118 116 return Err(AppError::Unauthorized);
@@ -9,7 +9,7 @@
9 9
10 10 use std::sync::Arc;
11 11
12 - use crate::error::{AppError, Result};
12 + use crate::error::{AppError, Result, ResultExt};
13 13
14 14 /// Format an optional display name as a greeting suffix: " Alice" or "".
15 15 fn greeting(name: Option<&str>) -> String {
@@ -216,7 +216,7 @@
216 216 .json(&payload)
217 217 .send()
218 218 .await
219 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to send email: {}", e)))?;
219 + .context("postmark http request")?;
220 220
221 221 if response.status().is_success() {
222 222 tracing::info!(recipient = %to, subject = %subject, "email sent");
@@ -177,18 +177,12 @@
177 177
178 178 // Check if user has 2FA enabled — redirect to verification page if so
179 179 if user.totp_enabled {
180 - session.cycle_id().await
181 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session cycle error: {}", e)))?;
182 - session.insert("pending_2fa_user_id", user.id).await
183 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
184 - session.insert("pending_2fa_notify_enabled", user.login_notification_enabled).await
185 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
186 - session.insert("pending_2fa_notify_email", &user.email).await
187 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
188 - session.insert("pending_2fa_notify_name", &user.display_name).await
189 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
190 - session.insert("pending_2fa_remember_me", remember).await
191 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
180 + session.cycle_id().await.context("session cycle")?;
181 + session.insert("pending_2fa_user_id", user.id).await.context("session insert")?;
182 + session.insert("pending_2fa_notify_enabled", user.login_notification_enabled).await.context("session insert")?;
183 + session.insert("pending_2fa_notify_email", &user.email).await.context("session insert")?;
184 + session.insert("pending_2fa_notify_name", &user.display_name).await.context("session insert")?;
185 + session.insert("pending_2fa_remember_me", remember).await.context("session insert")?;
192 186
193 187 tracing::info!(user_id = %user.id, event = "login_2fa_pending", "User requires 2FA verification");
194 188
@@ -322,12 +316,12 @@
322 316 let (rcr, auth_state) = state
323 317 .webauthn
324 318 .start_discoverable_authentication()
325 - .map_err(|e| AppError::Internal(anyhow::anyhow!("WebAuthn auth start: {}", e)))?;
319 + .context("webauthn auth start")?;
326 320
327 321 session
328 322 .insert(PASSKEY_AUTH_STATE_KEY, &auth_state)
329 323 .await
330 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?;
324 + .context("session error")?;
331 325
332 326 Ok(axum::Json(rcr).into_response())
333 327 }
@@ -343,7 +337,7 @@
343 337 let auth_state: DiscoverableAuthentication = session
344 338 .get(PASSKEY_AUTH_STATE_KEY)
345 339 .await
346 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Session error: {}", e)))?
340 + .context("session error")?
347 341 .ok_or_else(|| AppError::BadRequest("No pending passkey authentication".to_string()))?;
348 342
349 343 // Clean up session state
@@ -367,7 +361,7 @@
367 361
368 362 // Parse credential and convert for discoverable verification
369 363 let mut passkey: Passkey = serde_json::from_value(cred_json)
370 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Credential deserialize: {}", e)))?;
364 + .context("deserialize passkey credential")?;
371 365 let discoverable_key = DiscoverableKey::from(&passkey);
372 366
373 367 // Verify the authentication response
@@ -379,7 +373,7 @@
379 373 // Update the credential counter to prevent cloning attacks
380 374 passkey.update_credential(&auth_result);
381 375 let updated_json = serde_json::to_value(&passkey)
382 - .map_err(|e| AppError::Internal(anyhow::anyhow!("Credential serialize: {}", e)))?;
376 + .context("serialize passkey credential")?;
383 377 db::passkeys::update_passkey_after_auth(&state.db, &cred_id_bytes, &updated_json)
384 378 .await
385 379 .context("update passkey counter after auth")?;
@@ -430,7 +430,7 @@
430 430 .config
431 431 .synckit_jwt_secret
432 432 .as_deref()
433 - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("SyncKit not configured")))?;
433 + .ok_or_else(|| AppError::ServiceUnavailable("SyncKit is not configured".to_string()))?;
434 434
435 435 // Atomically consume code (must exist, not expired, not used).
436 436 // Single UPDATE...RETURNING prevents TOCTOU race on concurrent requests.
@@ -268,7 +268,7 @@
268 268 .ok_or_else(|| AppError::NotFound)?;
269 269
270 270 let s3 = state.s3.as_ref()
271 - .ok_or_else(|| AppError::BadRequest("Storage not configured".to_string()))?;
271 + .ok_or_else(|| AppError::ServiceUnavailable("File storage is not configured".to_string()))?;
272 272
273 273 let download_url = s3.presign_download(s3_key, Some(3600)).await?;
274 274