From 2a9e71f5b8b20d3c73672ecf10cefc60b4cf663c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 11 Jul 2026 23:48:34 -0400 Subject: [PATCH] fix pagination cursors (#1776) --- .../tts/list_user_tts_inference_results.rs | 18 ++------ .../list/list_available_weights_handler.rs | 9 ++-- .../src/http_server/web_utils/mod.rs | 1 + .../web_utils/web_sort_key_crypto.rs | 43 +++++++++++++++++++ .../src/startup/build_dependencies.rs | 4 +- .../storyteller_web/src/state/server_state.rs | 4 +- 6 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 crates/service/web/storyteller_web/src/http_server/web_utils/web_sort_key_crypto.rs diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs index 247e65ac4b..69e16fe049 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs @@ -84,11 +84,7 @@ pub async fn list_user_tts_inference_results_handler( let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false); let cursor = if let Some(cursor) = query.cursor.as_deref() { - let cursor = server_state.sort_key_crypto.decrypt_id(cursor) - .map_err(|e| { - warn!("crypto error: {:?}", e); - CommonWebError::from_anyhow_error(e) - })?; + let cursor = server_state.sort_key_crypto.decrypt_id(cursor)?; Some(cursor) } else { None @@ -117,22 +113,14 @@ pub async fn list_user_tts_inference_results_handler( }; let cursor_next = if let Some(id) = results_page.last_id { - let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) - .map_err(|e| { - warn!("crypto error: {:?}", e); - CommonWebError::from_anyhow_error(e) - })?; + let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?; Some(cursor) } else { None }; let cursor_previous = if let Some(id) = results_page.first_id { - let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) - .map_err(|e| { - warn!("crypto error: {:?}", e); - CommonWebError::from_anyhow_error(e) - })?; + let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?; Some(cursor) } else { None diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/weights/list/list_available_weights_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/weights/list/list_available_weights_handler.rs index da75a5ece9..31d7658f8b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/weights/list/list_available_weights_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/weights/list/list_available_weights_handler.rs @@ -107,6 +107,7 @@ pub struct ModelWeightForList { #[derive(Debug,ToSchema)] pub enum ListWeightError { + BadInput, NotAuthorized, ServerError, } @@ -120,6 +121,7 @@ impl std::fmt::Display for ListWeightError { impl ResponseError for ListWeightError { fn status_code(&self) -> StatusCode { match *self { + ListWeightError::BadInput => StatusCode::BAD_REQUEST, ListWeightError::NotAuthorized => StatusCode::UNAUTHORIZED, ListWeightError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, } @@ -165,11 +167,10 @@ pub async fn list_available_weights_handler( let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false); let cursor = if let Some(cursor) = query.cursor.as_deref() { + // Cursors arrive from clients verbatim; garbage is bad input, not a + // server fault. let cursor = server_state.sort_key_crypto.decrypt_id(cursor) - .map_err(|e| { - warn!("crypto error: {:?}", e); - ListWeightError::ServerError - })?; + .map_err(|_| ListWeightError::BadInput)?; Some(cursor) } else { None diff --git a/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs index 0b4c641330..6121977c03 100644 --- a/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs @@ -13,3 +13,4 @@ pub mod response_success_helpers; pub mod scoped_temp_dir_creator; pub mod serialize_as_json_error; pub mod try_delete_session_cache; +pub mod web_sort_key_crypto; diff --git a/crates/service/web/storyteller_web/src/http_server/web_utils/web_sort_key_crypto.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/web_sort_key_crypto.rs new file mode 100644 index 0000000000..ccbe3f30b1 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/web_utils/web_sort_key_crypto.rs @@ -0,0 +1,43 @@ +use log::warn; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::util::encrypted_sort_id::SortKeyCrypto; + +/// [`SortKeyCrypto`] with a web-facing interface: results map to +/// [`CommonWebError`] so handlers can use `?` directly, with the right HTTP +/// status for each direction. This is the flavor injected into Actix via +/// `ServerState`. +#[derive(Clone)] +pub struct WebSortKeyCrypto { + crypto: SortKeyCrypto, +} + +impl WebSortKeyCrypto { + pub fn new(secret: &str) -> Self { + Self { + crypto: SortKeyCrypto::new(secret), + } + } + + /// Encrypt a row id into an opaque pagination cursor. + /// Failure is a genuine server fault (500). + pub fn encrypt_id(&self, id: u64) -> Result { + self.crypto.encrypt_id(id) + .map_err(|err| { + warn!("Failed to encrypt pagination cursor: {:?}", err); + CommonWebError::from_anyhow_error(err) + }) + } + + /// Decrypt a client-supplied pagination cursor. + /// Cursors arrive from clients verbatim, so garbage is bad input (400), + /// not a server fault — a 500 here would trip the error-alerting + /// middleware on any bot or stale client. + pub fn decrypt_id(&self, cursor: &str) -> Result { + self.crypto.decrypt_id(cursor) + .map_err(|err| { + warn!("Invalid pagination cursor: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("Invalid pagination cursor.".to_string()) + }) + } +} diff --git a/crates/service/web/storyteller_web/src/startup/build_dependencies.rs b/crates/service/web/storyteller_web/src/startup/build_dependencies.rs index baeb73850d..161f74ae56 100644 --- a/crates/service/web/storyteller_web/src/startup/build_dependencies.rs +++ b/crates/service/web/storyteller_web/src/startup/build_dependencies.rs @@ -46,7 +46,7 @@ use crate::state::server_state::{ ServerState, TrollBans, }; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; -use crate::util::encrypted_sort_id::SortKeyCrypto; +use crate::http_server::web_utils::web_sort_key_crypto::WebSortKeyCrypto; // Bucket config const ENV_ACCESS_KEY: &str = "ACCESS_KEY"; @@ -156,7 +156,7 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult