fix pagination cursors (#1776)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-07-11 23:48:34 -04:00
1 parent 7dffc5fb5c
commit 2a9e71f5b8
6 files changed
+56 -23

No files matched your search

@@ -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
@@ -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
@@ -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;
@@ -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<String, CommonWebError> {
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<u64, CommonWebError> {
self.crypto.decrypt_id(cursor)
.map_err(|err| {
warn!("Invalid pagination cursor: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("Invalid pagination cursor.".to_string())
})
}
}
@@ -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<SetupResu
);
let sort_key_crypto_secret = easyenv::get_env_string_or_default("SORT_KEY_SECRET", "webscale");
let sort_key_crypto = SortKeyCrypto::new(&sort_key_crypto_secret);
let sort_key_crypto = WebSortKeyCrypto::new(&sort_key_crypto_secret);
let opaque_cursor_encoder = OpaqueCursorEncoderV2::new(&sort_key_crypto_secret);
let health_check_interval = easyenv::get_env_duration_seconds_or_default(
@@ -11,7 +11,7 @@ use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
use crate::state::flags::paging_flags::PagingFlags;
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
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;
use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList;
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
@@ -98,7 +98,7 @@ pub struct ServerState {
/// Where to store audio uploads for w2l
pub audio_uploads_bucket_root: String,
pub sort_key_crypto: SortKeyCrypto,
pub sort_key_crypto: WebSortKeyCrypto,
pub opaque_cursors: OpaqueCursorEncoderV2,
pub ip_ban_list: IpBanList,