From 0ac3e2c524c7075937fe91f8d73e076a650972ec Mon Sep 17 00:00:00 2001 From: Trezy Date: Fri, 8 May 2026 09:39:26 -0500 Subject: [PATCH] fix: better handling for PDS errors Signed-off-by: Trezy --- src/error.rs | 5 +++++ src/lua/execute.rs | 22 +++++++++++++++++++--- src/lua/record.rs | 19 +++++++++++++------ src/oauth/pds_write.rs | 18 +++++++++++++++++- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/error.rs b/src/error.rs index 5a814fb..caf4b39 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,6 +25,11 @@ impl std::fmt::Display for ScriptErrorType { /// Parse a Lua error message to extract a line number. /// +/// Prefix used to tag auth errors that pass through the Lua runtime boundary. +/// The script executor checks for this prefix to recover `AppError::Auth` from +/// a generic `mlua::Error::runtime`, returning 401 instead of 500. +pub const LUA_AUTH_ERROR_PREFIX: &str = "AUTH_ERROR:"; + /// mlua errors look like: /// - `[string "..."]:42: attempt to index a nil value` /// - `runtime error: [string "..."]:10: bad argument` diff --git a/src/lua/execute.rs b/src/lua/execute.rs index 51bdbb2..c1548ab 100644 --- a/src/lua/execute.rs +++ b/src/lua/execute.rs @@ -9,7 +9,7 @@ use std::time::Instant; use crate::AppState; use crate::auth::Claims; use crate::db::{DatabaseBackend, adapt_sql, now_rfc3339}; -use crate::error::{AppError, ScriptErrorType, parse_lua_line}; +use crate::error::{AppError, LUA_AUTH_ERROR_PREFIX, ScriptErrorType, parse_lua_line}; use crate::event_log::{EventLog, Severity, log_event}; use crate::lexicon::ParsedLexicon; use crate::repo; @@ -411,7 +411,15 @@ pub async fn execute_procedure_script( let msg = e.to_string(); tracing::error!(method, error = %msg, "lua script execution failed"); let (line, clean_msg) = parse_lua_line(&msg); - let app_error = if msg.contains("execution limit") { + let app_error = if msg.contains(LUA_AUTH_ERROR_PREFIX) + || clean_msg.contains(LUA_AUTH_ERROR_PREFIX) + { + let auth_msg = clean_msg + .strip_prefix(LUA_AUTH_ERROR_PREFIX) + .unwrap_or(&clean_msg) + .to_string(); + AppError::Auth(auth_msg) + } else if msg.contains("execution limit") { AppError::ScriptError { error_type: ScriptErrorType::Timeout, message: "script exceeded execution time limit".to_string(), @@ -766,7 +774,15 @@ pub async fn execute_query_script( let msg = e.to_string(); tracing::error!(method, error = %msg, "lua script execution failed"); let (line, clean_msg) = parse_lua_line(&msg); - let app_error = if msg.contains("execution limit") { + let app_error = if msg.contains(LUA_AUTH_ERROR_PREFIX) + || clean_msg.contains(LUA_AUTH_ERROR_PREFIX) + { + let auth_msg = clean_msg + .strip_prefix(LUA_AUTH_ERROR_PREFIX) + .unwrap_or(&clean_msg) + .to_string(); + AppError::Auth(auth_msg) + } else if msg.contains("execution limit") { AppError::ScriptError { error_type: ScriptErrorType::Timeout, message: "script exceeded execution time limit".to_string(), diff --git a/src/lua/record.rs b/src/lua/record.rs index 654c68a..16c7e3d 100644 --- a/src/lua/record.rs +++ b/src/lua/record.rs @@ -6,11 +6,20 @@ use std::sync::Arc; use crate::AppState; use crate::auth::Claims; use crate::db::{adapt_sql, now_rfc3339}; +use crate::error::{AppError, LUA_AUTH_ERROR_PREFIX}; use crate::record_refs::sync_refs; use crate::repo::PdsAuth; use super::tid::generate_tid; +fn pds_error(context: &str, e: AppError) -> mlua::Error { + if matches!(&e, AppError::Auth(_)) { + mlua::Error::runtime(format!("{LUA_AUTH_ERROR_PREFIX}{e}")) + } else { + mlua::Error::runtime(format!("PDS {context} failed: {e}")) + } +} + const INTERNAL_FIELDS: &[&str] = &[ "_collection", "_uri", @@ -85,7 +94,7 @@ pub fn register_record_api( let resp = pds_auth .post_json(&state, repo, "com.atproto.repo.putRecord", &pds_body) .await - .map_err(|e| mlua::Error::runtime(format!("PDS putRecord failed: {e}")))?; + .map_err(|e| pds_error("putRecord", e))?; if !resp.status().is_success() { let status = resp.status(); @@ -148,7 +157,7 @@ pub fn register_record_api( let resp = pds_auth .post_json(&state, repo, "com.atproto.repo.createRecord", &pds_body) .await - .map_err(|e| mlua::Error::runtime(format!("PDS createRecord failed: {e}")))?; + .map_err(|e| pds_error("createRecord", e))?; if !resp.status().is_success() { let status = resp.status(); @@ -249,7 +258,7 @@ pub fn register_record_api( let resp = pds_auth .post_json(&state, repo, "com.atproto.repo.deleteRecord", &pds_body) .await - .map_err(|e| mlua::Error::runtime(format!("PDS deleteRecord failed: {e}")))?; + .map_err(|e| pds_error("deleteRecord", e))?; if !resp.status().is_success() { let status = resp.status(); @@ -516,9 +525,7 @@ pub fn register_record_api( let resp = pds_auth .post_json(&state, repo, "com.atproto.repo.putRecord", &pds_body) .await - .map_err(|e| { - mlua::Error::runtime(format!("PDS putRecord failed: {e}")) - })?; + .map_err(|e| pds_error("putRecord", e))?; if !resp.status().is_success() { let status = resp.status(); diff --git a/src/oauth/pds_write.rs b/src/oauth/pds_write.rs index d619510..a0d2aa3 100644 --- a/src/oauth/pds_write.rs +++ b/src/oauth/pds_write.rs @@ -183,7 +183,23 @@ async fn retry_after_refresh( ); creds.session = fresh_session; } else { - return Err(e); + // Session is unrecoverable — clean it up so future requests + // fail fast instead of repeating the same doomed refresh. + tracing::warn!( + user_did = %creds.session.user_did, + api_client_id = %creds.session.api_client_id, + "refresh token permanently invalid, deleting broken session" + ); + let _ = super::sessions::delete_dpop_session( + pool, + backend, + &creds.session.api_client_id, + &creds.session.user_did, + ) + .await; + return Err(AppError::Auth( + "session expired, please re-authenticate".into(), + )); } } else { return Err(e); -- 2.51.2