observability into more unknown pages (#1354)

This commit is contained in:
Brandon Thomas
2026-04-13 17:35:54 -04:00
committed by GitHub
parent 45651103f0
commit 640e392d6c
12 changed files with 285 additions and 463 deletions
@@ -603,10 +603,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
AppStateSubscriptionProductKey,
AppStateUserInfo,
AppStateUserLocale,
BatchGetInferenceJobStatusError,
BatchGetInferenceJobStatusQueryParams,
BatchGetInferenceJobStatusSuccessResponse,
BatchGetMediaFilesError,
BatchGetMediaFilesModelInfo,
CreatePromptRequest,
CreatePromptResponse,
@@ -839,13 +837,11 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
GenerateVeo2Duration,
GenerateVeo2ImageToVideoRequest,
GenerateVeo2ImageToVideoResponse,
GetInferenceJobStatusError,
GetInferenceJobStatusPathInfo,
GetInferenceJobStatusSuccessResponse,
GetIsFeaturedItemError,
GetIsFeaturedItemPathInfo,
GetIsFeaturedItemSuccessResponse,
GetMediaFileError,
GetMediaFileModelInfo,
GetMediaFileModeratorFields,
GetMediaFilePathInfo,
@@ -884,23 +880,18 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListDatasetsByUserError,
ListDatasetsByUserPathInfo,
ListDatasetsByUserSuccessResponse,
ListFeaturedMediaFilesError,
ListFeaturedMediaFilesQueryParams,
ListFeaturedMediaFilesSuccessResponse,
ListFeaturedWeightsError,
ListFeaturedWeightsQueryParams,
ListFeaturedWeightsSuccessResponse,
ListMediaFilesByBatchError,
ListMediaFilesByBatchPathInfo,
ListMediaFilesByBatchSuccessResponse,
ListMediaFilesError,
ListMediaFilesForUserError,
ListMediaFilesForUserPathInfo,
ListMediaFilesForUserQueryParams,
ListMediaFilesForUserSuccessResponse,
ListMediaFilesQueryParams,
ListMediaFilesSuccessResponse,
ListPinnedMediaFilesError,
ListPinnedMediaFilesSuccessResponse,
ListPinnedWeightsError,
ListPinnedWeightsSuccessResponse,
@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
@@ -10,12 +10,9 @@ use crate::http_server::endpoints::inference_job::utils::extractors::extract_liv
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use actix_web_lab::extract::Query;
use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
@@ -26,7 +23,7 @@ use enums::by_table::generic_inference_jobs::frontend_failure_category::Frontend
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use log::error;
use log::{error, warn};
use mysql_queries::queries::generic_inference::web::batch_get_inference_job_status::batch_get_inference_job_status;
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
use redis::Commands;
@@ -143,37 +140,6 @@ pub struct BatchResultDetailsResponse {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, ToSchema)]
pub enum BatchGetInferenceJobStatusError {
ServerError,
NotFound,
}
impl ResponseError for BatchGetInferenceJobStatusError {
fn status_code(&self) -> StatusCode {
match *self {
BatchGetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
BatchGetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
Self::ServerError => "server error".to_string(),
Self::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for BatchGetInferenceJobStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Get job statuses for a batch of multiple job tokens.
#[utoipa::path(
get,
@@ -181,7 +147,7 @@ impl fmt::Display for BatchGetInferenceJobStatusError {
path = "/v1/jobs/batch",
responses(
(status = 200, body = BatchGetInferenceJobStatusSuccessResponse),
(status = 500, body = BatchGetInferenceJobStatusError),
(status = 500),
),
params(
BatchGetInferenceJobStatusQueryParams
@@ -190,8 +156,8 @@ impl fmt::Display for BatchGetInferenceJobStatusError {
pub async fn batch_get_inference_job_status_handler(
http_request: HttpRequest,
query: Query<BatchGetInferenceJobStatusQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<BatchGetInferenceJobStatusSuccessResponse>, BatchGetInferenceJobStatusError> {
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<BatchGetInferenceJobStatusSuccessResponse>, AdvancedCommonWebError> {
let tokens = query.tokens.iter()
.map(|token| token.trim())
@@ -205,31 +171,27 @@ pub async fn batch_get_inference_job_status_handler(
if tokens.is_empty() {
// NB: MediaDomain doesn't matter since it's an empty list.
return Ok(records_to_response(
Vec::new(),
server_state.server_environment,
Vec::new(),
server_state.server_environment,
MediaDomain::Storyteller
));
}
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_records = batch_get_inference_job_status(
let records = batch_get_inference_job_status(
&tokens,
&server_state.mysql_pool
).await;
let records = match maybe_records {
Ok(records) => records,
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(BatchGetInferenceJobStatusError::ServerError);
}
};
).await
.map_err(|err| {
warn!("Batch job query error: {:?}", err);
AdvancedCommonWebError::from_anyhow_error(err)
})?;
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
BatchGetInferenceJobStatusError::ServerError
warn!("Redis pool error: {:?}", e);
AdvancedCommonWebError::from_error(e)
})?;
// TODO(bt,2024-04-22): Look up the extra redis statuses per item.
@@ -1,6 +1,6 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
@@ -9,12 +9,9 @@ use crate::http_server::endpoints::inference_job::utils::extractors::extract_liv
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
@@ -24,7 +21,7 @@ use enums::by_table::generic_inference_jobs::frontend_failure_category::Frontend
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use log::error;
use log::{error, warn};
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
use redis::{Commands, RedisResult};
@@ -134,37 +131,6 @@ pub struct ResultDetailsResponse {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, ToSchema)]
pub enum GetInferenceJobStatusError {
ServerError,
NotFound,
}
impl ResponseError for GetInferenceJobStatusError {
fn status_code(&self) -> StatusCode {
match *self {
GetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
GetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
Self::ServerError => "server error".to_string(),
Self::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GetInferenceJobStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Get the status for a single job.
#[utoipa::path(
get,
@@ -175,19 +141,20 @@ impl fmt::Display for GetInferenceJobStatusError {
),
responses(
(status = 200, body = GetInferenceJobStatusSuccessResponse),
(status = 500, body = GetInferenceJobStatusError),
(status = 404),
(status = 500),
),
)]
pub async fn get_inference_job_status_handler(
http_request: HttpRequest,
path: Path<GetInferenceJobStatusPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<Json<GetInferenceJobStatusSuccessResponse>, GetInferenceJobStatusError>
{
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<GetInferenceJobStatusSuccessResponse>, AdvancedCommonWebError> {
if path.token.as_str().trim() == "None" {
// NB: A bunch of Python clients use our API and can fail in this manner.
// This was a large traffic driver during the 2023-03-08 outage.
// Presumably it's this client: https://github.com/shards-7/fakeyou.py
return Err(GetInferenceJobStatusError::NotFound);
// Presumably, it's this client: https://github.com/shards-7/fakeyou.py
return Err(AdvancedCommonWebError::NotFound);
}
// NB: Since this is publicly exposed, we don't query sensitive data.
@@ -198,18 +165,18 @@ pub async fn get_inference_job_status_handler(
let record = match maybe_status {
Ok(Some(record)) => record,
Ok(None) => return Err(GetInferenceJobStatusError::NotFound),
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(GetInferenceJobStatusError::ServerError);
warn!("Job query error for token {}: {:?}", path.token.as_str(), err);
return Err(AdvancedCommonWebError::from_anyhow_error(err));
}
};
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
GetInferenceJobStatusError::ServerError
warn!("Redis pool error: {:?}", e);
AdvancedCommonWebError::from_error(e)
})?;
// TODO(bt,2023-05-21): Make async.
@@ -1,11 +1,8 @@
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use actix_web_lab::extract::Query;
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
@@ -27,6 +24,7 @@ use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
@@ -34,7 +32,6 @@ use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::common_responses::live_portrait::MediaFileLivePortraitDetails;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
// =============== Request ===============
@@ -197,39 +194,6 @@ pub struct BatchGetMediaFilesModelInfo {
pub maybe_weight_creator: Option<UserDetailsLight>,
}
// =============== Error Response ===============
#[derive(Debug, ToSchema)]
pub enum BatchGetMediaFilesError {
ServerError,
NotFound,
}
impl ResponseError for BatchGetMediaFilesError {
fn status_code(&self) -> StatusCode {
match *self {
BatchGetMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
BatchGetMediaFilesError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
BatchGetMediaFilesError::ServerError => "server error".to_string(),
BatchGetMediaFilesError::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for BatchGetMediaFilesError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
/// List many media files at the same time by supplying their tokens. You'll have to provide the tokens.
@@ -239,8 +203,8 @@ impl fmt::Display for BatchGetMediaFilesError {
path = "/v1/media_files/batch",
responses(
(status = 200, description = "Found", body = BatchGetMediaFilesSuccessResponse),
(status = 404, description = "Not found", body = BatchGetMediaFilesError),
(status = 500, description = "Server error", body = BatchGetMediaFilesError),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
BatchGetMediaFilesQueryParams,
@@ -250,15 +214,11 @@ pub async fn batch_get_media_files_handler(
http_request: HttpRequest,
query: Query<BatchGetMediaFilesQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<BatchGetMediaFilesSuccessResponse>, BatchGetMediaFilesError> {
) -> Result<Json<BatchGetMediaFilesSuccessResponse>, AdvancedCommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
BatchGetMediaFilesError::ServerError
})?;
.await?;
let mut show_deleted_results = false;
let mut is_moderator = false;
@@ -288,7 +248,7 @@ pub async fn batch_get_media_files_handler(
Ok(media_files) => media_files,
Err(e) => {
warn!("query error: {:?}", e);
return Err(BatchGetMediaFilesError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
@@ -1,6 +1,6 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
@@ -10,12 +10,9 @@ use crate::http_server::endpoints::media_files::common_responses::live_portrait:
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_str_path::bucket_url_from_str_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
@@ -202,37 +199,6 @@ pub struct GetMediaFileModelInfo {
pub maybe_weight_creator: Option<UserDetailsLight>,
}
#[derive(Debug, ToSchema)]
pub enum GetMediaFileError {
ServerError,
NotFound,
}
impl ResponseError for GetMediaFileError {
fn status_code(&self) -> StatusCode {
match *self {
GetMediaFileError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
GetMediaFileError::NotFound => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
GetMediaFileError::ServerError => "server error".to_string(),
GetMediaFileError::NotFound => "not found".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GetMediaFileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Get a single media file by token.
#[utoipa::path(
get,
@@ -240,8 +206,8 @@ impl fmt::Display for GetMediaFileError {
path = "/v1/media_files/file/{token}",
responses(
(status = 200, description = "Found", body = GetMediaFileSuccessResponse),
(status = 404, description = "Not found", body = GetMediaFileError),
(status = 500, description = "Server error", body = GetMediaFileError),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
("path" = GetMediaFilePathInfo, description = "Path for Request")
@@ -251,17 +217,13 @@ pub async fn get_media_file_handler(
http_request: HttpRequest,
path: Path<GetMediaFilePathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GetMediaFileSuccessResponse>, GetMediaFileError> {
) -> Result<Json<GetMediaFileSuccessResponse>, AdvancedCommonWebError> {
let media_file_token = path.into_inner().token;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
GetMediaFileError::ServerError
})?;
.await?;
let mut show_deleted_results = false;
let mut is_moderator = false;
@@ -305,7 +267,7 @@ async fn modern_media_file_lookup(
show_deleted_results: bool,
server_state: &ServerState,
media_domain: MediaDomain,
) -> Result<GetMediaFileSuccessResponse, GetMediaFileError> {
) -> Result<GetMediaFileSuccessResponse, AdvancedCommonWebError> {
let is_mod = show_deleted_results;
@@ -318,9 +280,9 @@ async fn modern_media_file_lookup(
let result = match result {
Err(e) => {
warn!("query error: {:?}", e);
return Err(GetMediaFileError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
Ok(None) => return Err(GetMediaFileError::NotFound),
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
Ok(Some(result)) => result,
};
@@ -368,7 +330,7 @@ async fn modern_media_file_lookup(
public_bucket_url: bucket_url_from_media_path(&public_bucket_path, media_domain, server_state.server_environment)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
GetMediaFileError::ServerError
AdvancedCommonWebError::from_anyhow_error(err)
})?,
public_bucket_path: public_bucket_path
.get_full_object_path_str()
@@ -453,7 +415,7 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
show_deleted_results: bool,
server_state: &ServerState,
media_domain: MediaDomain,
) -> Result<GetMediaFileSuccessResponse, GetMediaFileError> {
) -> Result<GetMediaFileSuccessResponse, AdvancedCommonWebError> {
let result = select_tts_result_by_token(
&media_file_token.as_str(),
@@ -464,9 +426,9 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
let result = match result {
Err(e) => {
warn!("query error (legacy TTS): {:?}", e);
return Err(GetMediaFileError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
Ok(None) => return Err(GetMediaFileError::NotFound),
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
Ok(Some(result)) => result,
};
@@ -506,7 +468,7 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
public_bucket_url: bucket_url_from_str_path(&public_bucket_path, media_domain, server_state.server_environment)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
GetMediaFileError::ServerError
AdvancedCommonWebError::from_anyhow_error(err)
})?,
public_bucket_path,
cover_image: MediaFileCoverImageDetails::from_legacy_token_str(&result.tts_result_token),
@@ -1,19 +1,14 @@
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details_builder::MediaFileCoverImageDetailsBuilder;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
use crate::http_server::endpoints::media_files::get::get_media_file_handler::GetMediaFilePathInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use actix_web_lab::extract::Query;
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use artcraft_api_defs::common::responses::simple_entity_stats::SimpleEntityStats;
@@ -45,7 +40,7 @@ pub async fn list_batch_generated_redux_media_files_handler(
http_request: HttpRequest,
path: Path<ListBatchGeneratedReduxMediaFilesPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<ListBatchGeneratedReduxMediaFilesSuccessResponse>, CommonWebError> {
) -> Result<Json<ListBatchGeneratedReduxMediaFilesSuccessResponse>, AdvancedCommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
@@ -53,11 +48,7 @@ pub async fn list_batch_generated_redux_media_files_handler(
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
.await?;
let mut show_deleted_results = false;
let mut is_moderator = false;
@@ -83,7 +74,7 @@ pub async fn list_batch_generated_redux_media_files_handler(
Ok(batch) => batch.media_files,
Err(e) => {
warn!("query error: {:?}", e);
return Err(CommonWebError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
@@ -1,5 +1,3 @@
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Query;
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web_lab::__reexports::tracing::info;
@@ -23,6 +21,7 @@ use time::ext::InstantExt;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
@@ -182,27 +181,6 @@ pub struct FeaturedMediaFile {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, ToSchema)]
pub enum ListFeaturedMediaFilesError {
NotAuthorized,
ServerError,
}
impl std::fmt::Display for ListFeaturedMediaFilesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl ResponseError for ListFeaturedMediaFilesError {
fn status_code(&self) -> StatusCode {
match *self {
ListFeaturedMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
ListFeaturedMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// List media files that were featured or promoted by the staff (paginated). This will power our global asset drawers and highlight reels.
#[utoipa::path(
get,
@@ -211,15 +189,15 @@ impl ResponseError for ListFeaturedMediaFilesError {
params(ListFeaturedMediaFilesQueryParams),
responses(
(status = 200, description = "List Featured Media Files", body = ListFeaturedMediaFilesSuccessResponse),
(status = 401, description = "Not authorized", body = ListFeaturedMediaFilesError),
(status = 500, description = "Server error", body = ListFeaturedMediaFilesError),
(status = 401, description = "Not authorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn list_featured_media_files_handler(
http_request: HttpRequest,
query: Query<ListFeaturedMediaFilesQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ListFeaturedMediaFilesError> {
) -> Result<HttpResponse, AdvancedCommonWebError> {
let cache_start = Instant::now();
@@ -246,22 +224,14 @@ pub async fn list_featured_media_files_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);
ListFeaturedMediaFilesError::ServerError
})?;
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);
ListFeaturedMediaFilesError::ServerError
})?;
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
Some(cursor)
} else {
None
@@ -349,8 +319,7 @@ pub async fn list_featured_media_files_handler(
}
};
let body = serde_json::to_string(&response)
.map_err(|e| ListFeaturedMediaFilesError::ServerError)?;
let body = serde_json::to_string(&response)?;
let mut response_builder = HttpResponse::Ok();
@@ -368,7 +337,7 @@ async fn database_lookup(
query: &ListFeaturedMediaFilesQueryParams,
server_state: &ServerState,
maybe_cloudflare_header: Option<&str>,
) -> Result<FeaturedMediaFileListPage, ListFeaturedMediaFilesError> {
) -> Result<FeaturedMediaFileListPage, AdvancedCommonWebError> {
// TODO(bt,2023-12-04): Enforce real maximums and defaults
let limit = query.page_size.unwrap_or(25);
@@ -377,11 +346,7 @@ async fn database_lookup(
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);
ListFeaturedMediaFilesError::ServerError
})?;
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)?;
Some(cursor as usize)
} else {
None
@@ -411,7 +376,7 @@ async fn database_lookup(
mysql_pool: &server_state.mysql_pool,
}).await.map_err(|err| {
error!("DB error: {:?}", err);
ListFeaturedMediaFilesError::ServerError
AdvancedCommonWebError::from_anyhow_error(err)
})?;
let query_duration = Instant::now().signed_duration_since(query_start);
@@ -1,9 +1,7 @@
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path, Query};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, HttpRequest};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
@@ -23,6 +21,7 @@ use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
@@ -30,7 +29,6 @@ use crate::http_server::common_responses::pagination_page::PaginationPage;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::allowed_studio_access::allowed_studio_access;
@@ -140,33 +138,6 @@ pub struct MediaFilesByBatchListItem {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, ToSchema)]
pub enum ListMediaFilesByBatchError {
ServerError,
}
impl ResponseError for ListMediaFilesByBatchError {
fn status_code(&self) -> StatusCode {
match *self {
ListMediaFilesByBatchError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
ListMediaFilesByBatchError::ServerError => "server error".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
impl std::fmt::Display for ListMediaFilesByBatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// List media files that were generated together in a "batch", e.g. stable diffusion outputs.
///
/// This uses a special "batch token" that these jobs return. Media files created together will also
@@ -180,7 +151,7 @@ impl std::fmt::Display for ListMediaFilesByBatchError {
),
responses(
(status = 200, description = "List Media Files by Batch", body = ListMediaFilesByBatchSuccessResponse),
(status = 500, description = "Server error", body = ListMediaFilesByBatchError),
(status = 500, description = "Server error"),
),
)]
pub async fn list_media_files_by_batch_token_handler(
@@ -188,16 +159,12 @@ pub async fn list_media_files_by_batch_token_handler(
path: Path<ListMediaFilesByBatchPathInfo>,
query: Query<ListMediaFilesByBatchQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<ListMediaFilesByBatchSuccessResponse>, ListMediaFilesByBatchError>
) -> Result<Json<ListMediaFilesByBatchSuccessResponse>, AdvancedCommonWebError>
{
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
ListMediaFilesByBatchError::ServerError
})?;
.await?;
// NB: Temporary rollout flag for certain file types (BVH, etc).
let mut is_allowed_studio_access = allowed_studio_access(
@@ -226,7 +193,7 @@ pub async fn list_media_files_by_batch_token_handler(
Ok(results) => results,
Err(e) => {
warn!("Query error: {:?}", e);
return Err(ListMediaFilesByBatchError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
@@ -1,8 +1,6 @@
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Path, Query};
use actix_web::web::{Json, Path, Query};
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
@@ -22,6 +20,7 @@ use mysql_queries::queries::media_files::list::list_media_files_for_user::{list_
use tokens::tokens::media_files::MediaFileToken;
use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
@@ -32,7 +31,6 @@ use crate::http_server::endpoints::media_files::helpers::get_scoped_engine_categ
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_classes::get_scoped_media_classes;
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_types::get_scoped_media_types;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::allowed_studio_access::allowed_studio_access;
@@ -175,33 +173,6 @@ pub struct MediaFileForUserListItem {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, ToSchema)]
pub enum ListMediaFilesForUserError {
ServerError,
}
impl ResponseError for ListMediaFilesForUserError {
fn status_code(&self) -> StatusCode {
match *self {
ListMediaFilesForUserError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
ListMediaFilesForUserError::ServerError => "server error".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
impl std::fmt::Display for ListMediaFilesForUserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// List all of a user's media files (paginated).
///
/// This endpoint uses the session to automatically show all files for the given user, but only
@@ -215,7 +186,7 @@ impl std::fmt::Display for ListMediaFilesForUserError {
),
responses(
(status = 200, description = "List Featured Media Files", body = ListMediaFilesForUserSuccessResponse),
(status = 500, description = "Server error", body = ListMediaFilesForUserError),
(status = 500, description = "Server error"),
),
)]
pub async fn list_media_files_for_user_handler(
@@ -223,16 +194,12 @@ pub async fn list_media_files_for_user_handler(
path: Path<ListMediaFilesForUserPathInfo>,
query: Query<ListMediaFilesForUserQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ListMediaFilesForUserError>
) -> Result<Json<ListMediaFilesForUserSuccessResponse>, AdvancedCommonWebError>
{
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
ListMediaFilesForUserError::ServerError
})?;
.await?;
let mut is_author = false;
let mut is_mod = false;
@@ -287,7 +254,7 @@ pub async fn list_media_files_for_user_handler(
Ok(results) => results,
Err(e) => {
warn!("Query error: {:?}", e);
return Err(ListMediaFilesForUserError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
@@ -372,19 +339,12 @@ pub async fn list_media_files_for_user_handler(
})
.collect::<Vec<_>>();
let response = ListMediaFilesForUserSuccessResponse {
Ok(Json(ListMediaFilesForUserSuccessResponse {
success: true,
results,
pagination: PaginationPage{
pagination: PaginationPage {
current: results_page.current_page,
total_page_count: results_page.total_page_count,
}
};
let body = serde_json::to_string(&response)
.map_err(|e| ListMediaFilesForUserError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}))
}
@@ -1,8 +1,6 @@
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Query;
use actix_web::web::{Json, Query};
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
@@ -11,17 +9,18 @@ use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationT
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::view_as::ViewAs;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use log::warn;
use mysql_queries::queries::media_files::list::list_media_files::{list_media_files, ListMediaFilesArgs};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
@@ -33,7 +32,6 @@ use crate::http_server::endpoints::media_files::helpers::get_scoped_engine_categ
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_classes::get_scoped_media_classes;
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_types::get_scoped_media_types;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::allowed_explore_media_access::allowed_explore_media_access;
@@ -174,36 +172,6 @@ pub struct MediaFileListItem {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, ToSchema)]
pub enum ListMediaFilesError {
ServerError,
NotAuthorized,
}
impl ResponseError for ListMediaFilesError {
fn status_code(&self) -> StatusCode {
match *self {
ListMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
ListMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
ListMediaFilesError::ServerError => "server error".to_string(),
ListMediaFilesError::NotAuthorized => "not authorized".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
impl std::fmt::Display for ListMediaFilesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// List all media files in the system globally, across all users (paginated).
///
/// This powers "explore files" page.
@@ -214,29 +182,26 @@ impl std::fmt::Display for ListMediaFilesError {
params(ListMediaFilesQueryParams),
responses(
(status = 200, description = "List Featured Media Files", body = ListMediaFilesSuccessResponse),
(status = 500, description = "Server error", body = ListMediaFilesError),
(status = 401, description = "Not authorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn list_media_files_handler(
http_request: HttpRequest,
query: Query<ListMediaFilesQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ListMediaFilesError>
) -> Result<Json<ListMediaFilesSuccessResponse>, AdvancedCommonWebError>
{
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
ListMediaFilesError::ServerError
})?;
.await?;
// ==================== FEATURE FLAG CHECK ==================== //
if !allowed_explore_media_access(maybe_user_session.as_ref()) {
warn!("Explore media access is not permitted for user");
return Err(ListMediaFilesError::NotAuthorized);
return Err(AdvancedCommonWebError::NotAuthorized);
}
let mut is_mod = false;
@@ -255,11 +220,7 @@ pub async fn list_media_files_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);
ListMediaFilesError::ServerError
})?;
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)?;
Some(cursor as usize)
} else {
None
@@ -293,27 +254,19 @@ pub async fn list_media_files_handler(
Ok(results) => results,
Err(e) => {
warn!("Query error: {:?}", e);
return Err(ListMediaFilesError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
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);
ListMediaFilesError::ServerError
})?;
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);
ListMediaFilesError::ServerError
})?;
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
Some(cursor)
} else {
None
@@ -387,7 +340,7 @@ pub async fn list_media_files_handler(
})
.collect::<Vec<_>>();
let response = ListMediaFilesSuccessResponse {
Ok(Json(ListMediaFilesSuccessResponse {
success: true,
results,
pagination: PaginationCursors {
@@ -395,12 +348,5 @@ pub async fn list_media_files_handler(
maybe_previous: cursor_previous,
cursor_is_reversed,
}
};
let body = serde_json::to_string(&response)
.map_err(|e| ListMediaFilesError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}))
}
@@ -1,8 +1,7 @@
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::web::Json;
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
@@ -20,6 +19,7 @@ use redis::Commands;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
@@ -122,27 +122,6 @@ pub struct PinnedMediaFile {
/// The key we store pinned media file tokens under
const REDIS_KEY : &str = "pinned_media_files_list";
#[derive(Debug, ToSchema)]
pub enum ListPinnedMediaFilesError {
NotAuthorized,
ServerError,
}
impl std::fmt::Display for ListPinnedMediaFilesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl ResponseError for ListPinnedMediaFilesError {
fn status_code(&self) -> StatusCode {
match *self {
ListPinnedMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
ListPinnedMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// List media files that were pinned by the staff (paginated).
#[utoipa::path(
get,
@@ -150,25 +129,25 @@ impl ResponseError for ListPinnedMediaFilesError {
path = "/v1/media_files/list_pinned",
responses(
(status = 200, description = "List Featured Media Files", body = ListPinnedMediaFilesSuccessResponse),
(status = 401, description = "Not authorized", body = ListPinnedMediaFilesError),
(status = 500, description = "Server error", body = ListPinnedMediaFilesError),
(status = 401, description = "Not authorized"),
(status = 500, description = "Server error"),
),
)]
pub async fn list_pinned_media_files_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, impl ResponseError> {
) -> Result<Json<ListPinnedMediaFilesSuccessResponse>, AdvancedCommonWebError> {
let mut redis = server_state.redis_pool.get()
.map_err(|err| {
error!("Could not obtain redis: {err}");
ListPinnedMediaFilesError::ServerError
AdvancedCommonWebError::server_error_with_message("pinned media files error")
})?;
let token_list : Option<String> = redis.get(REDIS_KEY)
.map_err(|err| {
error!("Could not get redis result: {err}");
ListPinnedMediaFilesError::ServerError
AdvancedCommonWebError::server_error_with_message("pinned media files error")
})?;
let media_file_tokens = token_list
@@ -192,14 +171,14 @@ pub async fn list_pinned_media_files_handler(
Ok(media_files) => media_files,
Err(e) => {
warn!("Query error: {:?}", e);
return Err(ListPinnedMediaFilesError::ServerError);
return Err(AdvancedCommonWebError::from_anyhow_error(e));
}
};
}
let media_domain = get_media_domain(&http_request);
let response = ListPinnedMediaFilesSuccessResponse {
Ok(Json(ListPinnedMediaFilesSuccessResponse {
success: true,
results: media_files.into_iter()
.map(|m| {
@@ -265,12 +244,5 @@ pub async fn list_pinned_media_files_handler(
updated_at: m.updated_at,
}
}).collect::<Vec<_>>(),
};
let body = serde_json::to_string(&response)
.map_err(|e| ListPinnedMediaFilesError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}))
}
@@ -1,4 +1,6 @@
use log::{debug, warn};
use regex::Regex;
use std::sync::LazyLock;
use pager::client::pager::Pager;
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
@@ -21,7 +23,13 @@ pub(crate) fn check_advanced_common_web_error(
return true;
}
let title = format!("UncaughtServerError on {} {}", method, path);
let maybe_error_name = error.cause()
.and_then(|cause| try_error_title(cause));
let title = match maybe_error_name {
Some(name) => format!("{} on {} {}", name, method, path),
None => format!("UncaughtServerError on {} {}", method, path),
};
// Clone the Arc'd causal error so the notification owns a reference to it.
let cause_arc = error.clone_cause_arc();
@@ -53,3 +61,174 @@ pub(crate) fn check_advanced_common_web_error(
true
}
/// Try to extract a human-readable error title from the causal error's Debug
/// representation. Looks for CamelCase type names like `EnvError::NotUnicode`.
fn try_error_title(error: &dyn std::error::Error) -> Option<String> {
let debug_str = format!("{:?}", error);
try_extract_error_name(&debug_str)
}
/// Regex that matches CamelCase type names with an optional `::Variant` suffix.
/// E.g. `CommonWebError::InvalidRequest`, `EnvError`, `KinoviError::RequestError`.
static ERROR_NAME_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[A-Z][0-9a-z]+(?:[A-Z][0-9a-z]+)*(?:::[A-Z][0-9a-z]+(?:[A-Z][0-9a-z]+)*)?")
.expect("error name regex should compile")
});
/// Extract the leading CamelCase error name (with optional `::Variant`) from
/// a Debug-formatted error string. Returns `None` if no match is found.
///
/// ```text
/// "CommonWebError::InvalidRequest(\"bad\")" → Some("CommonWebError::InvalidRequest")
/// "EnvError::ParseError { reason: \"foo\" }" → Some("EnvError::ParseError")
/// "just a plain string" → None
/// ```
fn try_extract_error_name(debug_str: &str) -> Option<String> {
ERROR_NAME_RE.find(debug_str)
.map(|m| m.as_str().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
// --- try_extract_error_name tests ---
#[test]
fn extract_just_error_name() {
assert_eq!(
try_extract_error_name("JustErrorName"),
Some("JustErrorName".to_string()),
);
}
#[test]
fn extract_error_with_variant_and_string_payload() {
assert_eq!(
try_extract_error_name(r#"CommonWebError::InvalidRequest("Invalid request")"#),
Some("CommonWebError::InvalidRequest".to_string()),
);
}
#[test]
fn extract_error_with_unit_variant() {
assert_eq!(
try_extract_error_name("EnvError::NotUnicode"),
Some("EnvError::NotUnicode".to_string()),
);
}
#[test]
fn extract_error_with_struct_payload() {
assert_eq!(
try_extract_error_name(r#"EnvError::ParseError { reason: "foo" }"#),
Some("EnvError::ParseError".to_string()),
);
}
#[test]
fn extract_error_with_tuple_payload() {
assert_eq!(
try_extract_error_name("KinoviError::RequestError(asdf)"),
Some("KinoviError::RequestError".to_string()),
);
}
#[test]
fn extract_error_with_nested_errors() {
// Should only grab the outermost error name.
assert_eq!(
try_extract_error_name(
"ComplexError::SomeError(SomeError::SomeVariant(SomeInnerError::SomeInnerVariant))"
),
Some("ComplexError::SomeError".to_string()),
);
}
#[test]
fn extract_error_with_struct_containing_nested() {
assert_eq!(
try_extract_error_name(
"WrappedError::Variant { inner: SomeInnerError::SomeInnerVariant }"
),
Some("WrappedError::Variant".to_string()),
);
}
#[test]
fn no_match_for_lowercase_string() {
assert_eq!(
try_extract_error_name("just a plain error message with no type name"),
None,
);
}
#[test]
fn no_match_for_empty_string() {
assert_eq!(try_extract_error_name(""), None);
}
#[test]
fn single_word_camel_case() {
// A single CamelCase word like "Error" should not match because
// the regex requires at least two segments (e.g. "Ab" = A + b).
// "Error" = E + rror — "rror" has 4 lowercase chars which is fine.
assert_eq!(
try_extract_error_name("Error"),
Some("Error".to_string()),
);
}
// --- try_error_title tests (with real error types) ---
#[test]
fn title_from_io_error() {
let err: Box<dyn std::error::Error> = Box::new(
std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
);
// io::Error Debug looks like: `Custom { kind: NotFound, error: "file not found" }`
// "Custom" doesn't match CamelCase with 2+ segments... but we get what we get.
let title = try_error_title(err.as_ref());
// The exact output depends on Debug formatting. Just verify it doesn't panic.
// Custom has one uppercase + lowercase sequence = "Custom" which matches [A-Z][0-9a-z]+
assert!(title.is_some());
}
#[test]
fn title_from_serde_json_error() {
let err: serde_json::Error = serde_json::from_str::<String>("not json").unwrap_err();
let title = try_error_title(&err);
// serde_json::Error Debug typically starts with "Error(...)"
assert!(title.is_some());
}
/// A simple custom error for testing.
#[derive(Debug)]
enum TestAppError {
DatabaseTimeout,
InvalidInput(String),
}
impl std::fmt::Display for TestAppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for TestAppError {}
#[test]
fn title_from_custom_unit_variant() {
let err = TestAppError::DatabaseTimeout;
let title = try_error_title(&err);
assert_eq!(title, Some("DatabaseTimeout".to_string()));
}
#[test]
fn title_from_custom_tuple_variant() {
let err = TestAppError::InvalidInput("bad data".to_string());
let title = try_error_title(&err);
assert_eq!(title, Some("InvalidInput".to_string()));
}
}