From 2d3ec8b4e56d92dcace33aa533a3d44b44f9e389 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 1 Oct 2025 22:31:20 -0400 Subject: [PATCH] update sora error type - much more semantic --- .../src/creds/sora_credential_builder.rs | 34 +++- .../src/creds/sora_credential_set.rs | 6 +- .../src/creds/sora_jwt_bearer_token.rs | 4 +- .../openai_sora_client/src/error/mod.rs | 4 + .../src/error/sora_client_error.rs | 49 ++++++ .../src/error/sora_error.rs | 54 ++++++ .../src/error/sora_generic_api_error.rs | 48 +++++ .../src/error/sora_specific_api_error.rs | 44 +++++ .../api_clients/openai_sora_client/src/lib.rs | 2 +- .../image_remix_with_session_auto_renew.rs | 28 +-- ...load_from_bytes_with_session_auto_renew.rs | 2 +- ...pload_from_file_with_session_auto_renew.rs | 5 +- ...ora_task_status_with_session_auto_renew.rs | 2 +- ...maybe_refresh_credentials_on_sora_error.rs | 22 ++- .../recipes/maybe_upgrade_or_renew_session.rs | 2 +- ...imple_image_gen_with_session_auto_renew.rs | 31 ++-- .../bearer/generate_bearer_with_cookie.rs | 27 ++- .../requests/image_gen/image_gen_status.rs | 40 +++-- .../image_gen/sora_image_gen_remix.rs | 6 +- .../image_gen/sora_image_gen_simple.rs | 6 +- .../src/requests/job/sora_job_status.rs | 28 ++- .../src/requests/list_media/list_media.rs | 35 +++- .../sentinel_refresh/generate/token.rs | 33 +++- .../upload/upload_media_from_bytes.rs | 2 +- .../requests/upload/upload_media_from_file.rs | 28 ++- .../upload/upload_media_http_request.rs | 37 +++- .../openai_sora_client/src/sora_error.rs | 166 ------------------ .../src/utils/classify_general_http_error.rs | 5 +- ...ssify_general_http_status_code_and_body.rs | 17 +- .../src/utils/lightweight_sora_jwt_parse.rs | 15 +- .../core/commands/enqueue/generate_error.rs | 2 +- .../enqueue_image_bg_removal_command.rs | 1 - .../enqueue_contextual_edit_image_command.rs | 1 - .../handle_gpt_image_1_edit_sora.rs | 1 - .../enqueue_image_inpaint_command.rs | 1 - .../sora/commands/sora_image_remix_command.rs | 9 +- .../get_sora_credentials_from_redis.rs | 6 +- .../get_sora_credentials_from_request.rs | 12 +- 38 files changed, 498 insertions(+), 317 deletions(-) create mode 100644 crates/api_clients/openai_sora_client/src/error/mod.rs create mode 100644 crates/api_clients/openai_sora_client/src/error/sora_client_error.rs create mode 100644 crates/api_clients/openai_sora_client/src/error/sora_error.rs create mode 100644 crates/api_clients/openai_sora_client/src/error/sora_generic_api_error.rs create mode 100644 crates/api_clients/openai_sora_client/src/error/sora_specific_api_error.rs delete mode 100644 crates/api_clients/openai_sora_client/src/sora_error.rs diff --git a/crates/api_clients/openai_sora_client/src/creds/sora_credential_builder.rs b/crates/api_clients/openai_sora_client/src/creds/sora_credential_builder.rs index 546dce1864..1b7ed58693 100644 --- a/crates/api_clients/openai_sora_client/src/creds/sora_credential_builder.rs +++ b/crates/api_clients/openai_sora_client/src/creds/sora_credential_builder.rs @@ -1,5 +1,9 @@ +use crate::creds::sora_cookies::SoraCookies; use crate::creds::sora_credential_set::SoraCredentialSet; -use crate::sora_error::SoraError; +use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; +use crate::creds::sora_sentinel::SoraSentinel; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; pub struct SoraCredentialBuilder { cookies: Option, @@ -16,29 +20,41 @@ impl SoraCredentialBuilder { } } - pub fn cookies(mut self, cookies: &str) -> Self { + // Consuming methods + + pub fn with_cookies(mut self, cookies: &str) -> Self { self.cookies = Some(cookies.to_string()); self } - pub fn jwt_bearer_token(mut self, token: &str) -> Self { + pub fn with_jwt_bearer_token(mut self, token: &str) -> Self { self.jwt_bearer_token = Some(token.to_string()); self } - pub fn sora_sentinel(mut self, sentinel: &str) -> Self { + pub fn with_sora_sentinel(mut self, sentinel: &str) -> Self { self.sora_sentinel = Some(sentinel.to_string()); self } + + // Mutable methods + + pub fn set_cookies(&mut self, cookies: &str) { + self.cookies = Some(cookies.to_string()); + } + + pub fn set_jwt_bearer_token(&mut self, token: &str) { + self.jwt_bearer_token = Some(token.to_string()); + } + + pub fn set_sora_sentinel(&mut self, sentinel: &str) { + self.sora_sentinel = Some(sentinel.to_string()); + } pub fn build(self) -> Result { - use crate::creds::sora_cookies::SoraCookies; - use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; - use crate::creds::sora_sentinel::SoraSentinel; - let cookies = match self.cookies { Some(c) => SoraCookies::new(c), - None => return Err(SoraError::SoraCredentialBuilderError("no cookies provided".to_string())), + None => return Err(SoraClientError::SoraCredentialBuilderError("no cookies provided").into()), }; let jwt_bearer_token = match self.jwt_bearer_token { diff --git a/crates/api_clients/openai_sora_client/src/creds/sora_credential_set.rs b/crates/api_clients/openai_sora_client/src/creds/sora_credential_set.rs index 15d8946a4c..3abd7fc730 100644 --- a/crates/api_clients/openai_sora_client/src/creds/sora_credential_set.rs +++ b/crates/api_clients/openai_sora_client/src/creds/sora_credential_set.rs @@ -39,12 +39,12 @@ impl SoraCredentialSet { pub fn to_builder(&self) -> SoraCredentialBuilder { let mut builder = SoraCredentialBuilder::new() - .cookies(self.cookies.as_str()); + .with_cookies(self.cookies.as_str()); if let Some(bearer) = &self.jwt_bearer_token { - builder = builder.jwt_bearer_token(bearer.as_str()); + builder = builder.with_jwt_bearer_token(bearer.as_str()); } if let Some(sentinel) = &self.sora_sentinel { - builder = builder.sora_sentinel(sentinel.as_str()); + builder = builder.with_sora_sentinel(sentinel.as_str()); } builder } diff --git a/crates/api_clients/openai_sora_client/src/creds/sora_jwt_bearer_token.rs b/crates/api_clients/openai_sora_client/src/creds/sora_jwt_bearer_token.rs index a5304f840d..39cc77aadd 100644 --- a/crates/api_clients/openai_sora_client/src/creds/sora_jwt_bearer_token.rs +++ b/crates/api_clients/openai_sora_client/src/creds/sora_jwt_bearer_token.rs @@ -1,5 +1,5 @@ +use crate::error::sora_error::SoraError; use crate::utils::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims}; -use errors::AnyhowResult; /// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session. #[derive(Clone)] @@ -9,7 +9,7 @@ pub struct SoraJwtBearerToken { } impl SoraJwtBearerToken { - pub fn new(token: String) -> AnyhowResult { + pub fn new(token: String) -> Result { let jwt_claims = lightweight_sora_jwt_parse(&token)?; Ok(SoraJwtBearerToken { token, diff --git a/crates/api_clients/openai_sora_client/src/error/mod.rs b/crates/api_clients/openai_sora_client/src/error/mod.rs new file mode 100644 index 0000000000..010e4fe745 --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod sora_client_error; +pub mod sora_error; +pub mod sora_generic_api_error; +pub mod sora_specific_api_error; \ No newline at end of file diff --git a/crates/api_clients/openai_sora_client/src/error/sora_client_error.rs b/crates/api_clients/openai_sora_client/src/error/sora_client_error.rs new file mode 100644 index 0000000000..56836220ea --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/error/sora_client_error.rs @@ -0,0 +1,49 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum SoraClientError { + /// An error reading the file for upload. + FileForUploadReadError(std::io::Error), + + /// The file path provided for upload is invalid. + FileForUploadHasInvalidPath, + + /// Something is wrong with the JWT bearer token. + /// This error originates on our end as we try to parse the JWT. + LocalJwtClaimsParseError(String), + + /// There was an error constructing the form-multipart request. + MultipartFormError(wreq::Error), + + /// We haven't received a bearer token yet + /// This is our own internal application state error, not something Sora returns. + /// We know our client can't make the request, so we preemptively fail it. + NoBearerTokenAvailable, + + /// Issue with using the SoraCredentialBuilder. + SoraCredentialBuilderError(&'static str), + + /// Error parsing a request URL. + UrlParseError(url::ParseError), + + /// An error was encountered in building the Wreq client + WreqClientError(wreq::Error), +} + +impl Error for SoraClientError {} + +impl Display for SoraClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err), + Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."), + Self::LocalJwtClaimsParseError(msg) => write!(f, "Local JWT claims parse error: {}", msg), + Self::MultipartFormError(err) => write!(f, "Multipart form error: {}", err), + Self::NoBearerTokenAvailable => write!(f, "No bearer token available. The client needs a bearer token to make the request."), + Self::SoraCredentialBuilderError(msg) => write!(f, "Sora Credential Builder error: {}", msg), + Self::UrlParseError(err) => write!(f, "URL parse error: {}", err), + Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err), + } + } +} diff --git a/crates/api_clients/openai_sora_client/src/error/sora_error.rs b/crates/api_clients/openai_sora_client/src/error/sora_error.rs new file mode 100644 index 0000000000..5fc7ae3978 --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/error/sora_error.rs @@ -0,0 +1,54 @@ +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; +use cloudflare_errors::cloudflare_error::CloudflareError; +use std::error::Error; + +#[derive(Debug)] +pub enum SoraError { + Client(SoraClientError), + ApiSpecific(SoraSpecificApiError), + ApiGeneric(SoraGenericApiError), +} + +impl SoraError { + pub fn is_sora_having_downtime_issues(&self) -> bool { + match self { + Self::ApiGeneric(SoraGenericApiError::Http502ErrorBadGateway(_)) => true, + Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true, + Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true, + Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true, + _ => false, + } + } +} + +impl Error for SoraError {} + +impl std::fmt::Display for SoraError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Client(e) => write!(f, "SoraClientError: {:?}", e), + Self::ApiSpecific(e) => write!(f, "SoraSpecificApiError: {:?}", e), + Self::ApiGeneric(e) => write!(f, "SoraGenericApiError: {:?}", e), + } + } +} + +impl From for SoraError { + fn from(error: SoraClientError) -> Self { + Self::Client(error) + } +} + +impl From for SoraError { + fn from(error: SoraSpecificApiError) -> Self { + Self::ApiSpecific(error) + } +} + +impl From for SoraError { + fn from(error: SoraGenericApiError) -> Self { + Self::ApiGeneric(error) + } +} diff --git a/crates/api_clients/openai_sora_client/src/error/sora_generic_api_error.rs b/crates/api_clients/openai_sora_client/src/error/sora_generic_api_error.rs new file mode 100644 index 0000000000..5fcbe77f4f --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/error/sora_generic_api_error.rs @@ -0,0 +1,48 @@ +use cloudflare_errors::cloudflare_error::CloudflareError; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use wreq::StatusCode; + +#[derive(Debug)] +pub enum SoraGenericApiError { + /// Specific Cloudflare errors. + CloudflareError(CloudflareError), + + /// HTTP 502 Bad Gateway + /// Sora is having issues. + /// Typically served from Cloudflare + /// We preserve the message for debugging. + Http502ErrorBadGateway(String), + + /// serde_json::Error, likely from JSON deserialization schema mismatch. + /// Includes the original body. + SerdeResponseParseErrorWithBody(serde_json::Error, String), + + /// Another error occurred. + /// TODO: Try to get rid of this. + UncategorizedBadResponse(String), + + /// An uncategorized bad HTTP response from Sora. + UncategorizedBadResponseWithStatusAndBody { + status_code: StatusCode, + body: String, + }, + + /// An uncaught error from the API client. + WreqError(wreq::Error), +} + +impl Error for SoraGenericApiError {} + +impl Display for SoraGenericApiError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err), + Self::Http502ErrorBadGateway(msg) => write!(f, "HTTP 502 Bad Gateway: {}", msg), + Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body), + Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body), + Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response status: {}", msg), + Self::WreqError(err) => write!(f, "Wreq client error: {}", err), + } + } +} diff --git a/crates/api_clients/openai_sora_client/src/error/sora_specific_api_error.rs b/crates/api_clients/openai_sora_client/src/error/sora_specific_api_error.rs new file mode 100644 index 0000000000..aaf0b28e4f --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/error/sora_specific_api_error.rs @@ -0,0 +1,44 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum SoraSpecificApiError { + /// Failed to generate a bearer token despite the `200 OK` response. + /// This has been generated by sending old/expired session cookies or empty cookies. + FailedToGenerateBearerToken, + + /// Sora requires users to set up an account username before they can generate images. + /// This requires the user to set up that username. + SoraUsernameNotYetCreated, + + /// We're sending too many tasks to Sora. + /// Sora is telling us to cool it. + TooManyConcurrentTasks, + + // TODO: Try to make concrete error types for these distinct cases. + /// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login. + /// + /// Example message, e.g. from the upload endpoint: + /// { + /// "error": { + /// "message": "Your authentication token has expired. Please try signing in again.", + /// "type": "invalid_request_error", + /// "param": null, + /// "code": "token_expired" + /// } + /// } + UnauthorizedCookieOrBearerExpired, +} + +impl Error for SoraSpecificApiError {} + +impl Display for SoraSpecificApiError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::FailedToGenerateBearerToken => write!(f, "Failed to generate bearer token. Probably invalid or expired session cookies."), + Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."), + Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."), + Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"), + } + } +} diff --git a/crates/api_clients/openai_sora_client/src/lib.rs b/crates/api_clients/openai_sora_client/src/lib.rs index 5ba8f4ca4e..7d270cd4be 100644 --- a/crates/api_clients/openai_sora_client/src/lib.rs +++ b/crates/api_clients/openai_sora_client/src/lib.rs @@ -1,5 +1,5 @@ pub mod creds; +pub mod error; pub mod recipes; pub mod requests; -pub mod sora_error; pub mod utils; \ No newline at end of file diff --git a/crates/api_clients/openai_sora_client/src/recipes/image_remix_with_session_auto_renew.rs b/crates/api_clients/openai_sora_client/src/recipes/image_remix_with_session_auto_renew.rs index 9f7ef8142e..ffa963e333 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/image_remix_with_session_auto_renew.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/image_remix_with_session_auto_renew.rs @@ -1,12 +1,14 @@ use crate::creds::sora_credential_set::SoraCredentialSet; use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; use crate::creds::sora_sentinel::SoraSentinel; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie; use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse}; use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest}; use crate::requests::image_gen::{image_gen_http_request, SoraImageGenError}; use crate::requests::sentinel_refresh::generate::token::generate_token; -use crate::sora_error::SoraError; use anyhow::anyhow; use errors::AnyhowResult; use log::{info, warn}; @@ -47,17 +49,17 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq match err { // We'll fail these requests... SoraImageGenError::TooManyConcurrentTasks(_) => { - return Err(SoraError::TooManyConcurrentTasks); + return Err(SoraSpecificApiError::TooManyConcurrentTasks.into()); + } + SoraImageGenError::UsernameRequired(_err) => { + return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into()) } SoraImageGenError::GenericError(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("image remix failed with GenericError: {:?}", err))) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("image remix failed with GenericError: {:?}", err)).into()) } SoraImageGenError::NetworkError(err) => { // TODO: The underlying type should be a reqwest::Error. - return Err(SoraError::OtherBadStatus(anyhow!("network error: {:?}", err))) - } - SoraImageGenError::UsernameRequired(_err) => { - return Err(SoraError::SoraUsernameNotYetCreated) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("network error: {:?}", err)).into()) } // We'll retry these requests... @@ -85,12 +87,12 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq let response = generate_bearer_with_cookie(cookies).await; match response { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err))) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new JWT bearer token: {:?}", err)).into()) } Ok(bearer) => { match SoraJwtBearerToken::new(bearer) { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err))); + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("Failed to parse new JWT bearer token: {:?}", err)).into()); } Ok(bearer) => new_bearer = Some(bearer), } @@ -105,7 +107,7 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq let response = generate_token().await; match response { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err))) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new sentinel: {:?}", err)).into()) } Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)), } @@ -135,12 +137,12 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq match result { Ok(response) => Ok((response, Some(new_creds))), Err(err) => match err { - image_gen_http_request::SoraImageGenError::TooManyConcurrentTasks(err) => { - Err(SoraError::TooManyConcurrentTasks) + SoraImageGenError::TooManyConcurrentTasks(err) => { + Err(SoraSpecificApiError::TooManyConcurrentTasks.into()) } _ => { warn!("Image remix failed again: {:?}", err); - Err(SoraError::OtherBadStatus(anyhow!("image remix failed again: {:?}", err))) + Err(SoraGenericApiError::UncategorizedBadResponse(format!("image remix failed again: {:?}", err)).into()) } } } diff --git a/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_bytes_with_session_auto_renew.rs b/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_bytes_with_session_auto_renew.rs index e5e5fd515f..824a6dacf0 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_bytes_with_session_auto_renew.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_bytes_with_session_auto_renew.rs @@ -9,11 +9,11 @@ use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, Sor use crate::requests::sentinel_refresh::generate::token::generate_token; use crate::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes; use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse}; -use crate::sora_error::SoraError; use anyhow::anyhow; use errors::AnyhowResult; use log::{error, info, warn}; use std::time::Duration; +use crate::error::sora_error::SoraError; pub struct ImageUploadFromBytesAutoRenewRequest<'a> { pub file_bytes: Vec, diff --git a/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_file_with_session_auto_renew.rs b/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_file_with_session_auto_renew.rs index a9eb985ca3..3434a9c322 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_file_with_session_auto_renew.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/image_upload_from_file_with_session_auto_renew.rs @@ -1,11 +1,12 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_error::SoraError; use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error; use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file; use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse}; -use crate::sora_error::SoraError; use log::{info, warn}; use std::path::Path; use std::time::Duration; +use crate::error::sora_specific_api_error::SoraSpecificApiError; pub struct ImageUploadFromFileAutoRenewRequest<'a, P: AsRef> { pub file_path: P, @@ -31,7 +32,7 @@ pub async fn image_upload_from_file_with_session_auto_renew>( let err = match result { Ok(response) => return Ok((response, None)), // We can't retry some errors. - Err(err @ SoraError::SoraUsernameNotYetCreated) => return Err(err), + Err(err @ SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated)) => return Err(err), // Retry all other errors. Err(err) => err, }; diff --git a/crates/api_clients/openai_sora_client/src/recipes/list_sora_task_status_with_session_auto_renew.rs b/crates/api_clients/openai_sora_client/src/recipes/list_sora_task_status_with_session_auto_renew.rs index 792210b9dd..79056cf389 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/list_sora_task_status_with_session_auto_renew.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/list_sora_task_status_with_session_auto_renew.rs @@ -1,11 +1,11 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_error::SoraError; use crate::recipes::image_upload_from_file_with_session_auto_renew::ImageUploadFromFileAutoRenewRequest; use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error; use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskId, TaskResponse, VideoGenStatusResponse}; use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file; use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse; -use crate::sora_error::SoraError; use anyhow::Error; use log::{info, warn}; use std::path::Path; diff --git a/crates/api_clients/openai_sora_client/src/recipes/maybe_refresh_credentials_on_sora_error.rs b/crates/api_clients/openai_sora_client/src/recipes/maybe_refresh_credentials_on_sora_error.rs index 57f9ce3833..22f1413097 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/maybe_refresh_credentials_on_sora_error.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/maybe_refresh_credentials_on_sora_error.rs @@ -1,7 +1,9 @@ use crate::creds::sora_credential_set::SoraCredentialSet; use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie; -use crate::sora_error::SoraError; use anyhow::anyhow; use log::{error, info, warn}; @@ -10,15 +12,15 @@ use log::{error, info, warn}; pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, error: SoraError) -> Result { match error { - SoraError::NoBearerTokenAvailable => { - warn!("Previous request failed due to missing bearer token."); + SoraError::Client(SoraClientError::NoBearerTokenAvailable) => { + error!("Previous request failed due to missing bearer token."); } - SoraError::UnauthorizedCookieOrBearerExpired => { - warn!("Previous request failed due to invalid bearer token."); + SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => { + error!("Previous request failed due to invalid bearer token."); } _ => { - error!("Previous request failed with error: {:?}", error); - return Err(SoraError::OtherBadStatus(anyhow!("previous request failed with error: {:?}", error))) + error!("Previous request failed due to the following error: {:?}", error); + return Err(error) } } @@ -29,12 +31,14 @@ pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, let new_bearer = match response { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err))) + error!("Error generating new JWT bearer token: {}", err); + return Err(err); } Ok(bearer) => { match SoraJwtBearerToken::new(bearer) { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to parse new JWT bearer token: {:?}", err))); + error!("Error parsing new JWT bearer token: {}", err); + return Err(err); } Ok(bearer) => bearer, } diff --git a/crates/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs b/crates/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs index f7061bfe13..fed78d23a4 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs @@ -7,7 +7,7 @@ use chrono::{DateTime, TimeDelta, Utc}; use errors::AnyhowResult; use log::info; use std::ops::Sub; -use crate::sora_error::SoraError; +use crate::error::sora_error::SoraError; const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12); diff --git a/crates/api_clients/openai_sora_client/src/recipes/simple_image_gen_with_session_auto_renew.rs b/crates/api_clients/openai_sora_client/src/recipes/simple_image_gen_with_session_auto_renew.rs index 19385413cb..b76bcd5d04 100644 --- a/crates/api_clients/openai_sora_client/src/recipes/simple_image_gen_with_session_auto_renew.rs +++ b/crates/api_clients/openai_sora_client/src/recipes/simple_image_gen_with_session_auto_renew.rs @@ -1,16 +1,18 @@ use crate::creds::sora_credential_set::SoraCredentialSet; use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; use crate::creds::sora_sentinel::SoraSentinel; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie; use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse}; use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest}; use crate::requests::image_gen::sora_image_gen_simple::{sora_image_gen_simple, SoraImageGenSimpleRequest}; use crate::requests::image_gen::{image_gen_http_request, SoraImageGenError}; use crate::requests::sentinel_refresh::generate::token::generate_token; -use crate::sora_error::SoraError; use anyhow::anyhow; use errors::AnyhowResult; -use log::{info, warn}; +use log::{error, info, warn}; use std::time::Duration; pub struct SimpleImageGenAutoRenewRequest<'a> { @@ -46,17 +48,17 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut match err { // We'll fail these requests... SoraImageGenError::TooManyConcurrentTasks(_) => { - return Err(SoraError::TooManyConcurrentTasks); + return Err(SoraSpecificApiError::TooManyConcurrentTasks.into()); + } + SoraImageGenError::UsernameRequired(_err) => { + return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into()) } SoraImageGenError::GenericError(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("image gen failed with GenericError: {:?}", err))) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("image gen failed with GenericError: {:?}", err)).into()) } SoraImageGenError::NetworkError(err) => { // TODO: The underlying type should be a reqwest::Error. - return Err(SoraError::OtherBadStatus(anyhow!("network error: {:?}", err))) - } - SoraImageGenError::UsernameRequired(_err) => { - return Err(SoraError::SoraUsernameNotYetCreated) + return Err(SoraGenericApiError::UncategorizedBadResponse(format!("network error (todo: this should be a reqwest error): {:?}", err)).into()) } // We'll retry these requests... @@ -84,12 +86,14 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut let response = generate_bearer_with_cookie(cookies).await; match response { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err))) + error!("Error asking to generate JWT: {:?}", err); + return Err(err); } Ok(bearer) => { match SoraJwtBearerToken::new(bearer) { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err))); + error!("Failed to parse new JWT: {:?}", err); + return Err(err); } Ok(bearer) => new_bearer = Some(bearer), } @@ -104,7 +108,8 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut let response = generate_token().await; match response { Err(err) => { - return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err))) + error!("failed to generate new sentinel: {:?}", err); + return Err(err) } Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)), } @@ -134,11 +139,11 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut Ok(response) => Ok((response, Some(new_creds))), Err(err) => match err { image_gen_http_request::SoraImageGenError::TooManyConcurrentTasks(err) => { - Err(SoraError::TooManyConcurrentTasks) + Err(SoraSpecificApiError::TooManyConcurrentTasks.into()) } _ => { warn!("Image gen failed again: {:?}", err); - Err(SoraError::OtherBadStatus(anyhow!("image gen failed again: {:?}", err))) + Err(SoraGenericApiError::UncategorizedBadResponse(format!("image gen failed again: {:?}", err)).into()) } } } diff --git a/crates/api_clients/openai_sora_client/src/requests/bearer/generate_bearer_with_cookie.rs b/crates/api_clients/openai_sora_client/src/requests/bearer/generate_bearer_with_cookie.rs index dacba759b1..2c93862122 100644 --- a/crates/api_clients/openai_sora_client/src/requests/bearer/generate_bearer_with_cookie.rs +++ b/crates/api_clients/openai_sora_client/src/requests/bearer/generate_bearer_with_cookie.rs @@ -1,5 +1,8 @@ +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; use crate::requests::image_gen::SoraImageGenError; -use crate::sora_error::SoraError; use crate::utils::classify_general_http_error::classify_general_http_error; use crate::utils::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body; use errors::AnyhowResult; @@ -34,7 +37,11 @@ pub struct SoraUser { pub async fn generate_bearer_with_cookie(cookie: &str) -> Result { let client = Client::builder() .gzip(true) - .build()?; + .build() + .map_err(|err| { + error!("Error building the client: {:?}", err); + SoraClientError::WreqClientError(err) + })?; info!("Generating new JWT bearer token from session cookies... (cookie payload length: {})", cookie.len()); @@ -45,14 +52,18 @@ pub async fn generate_bearer_with_cookie(cookie: &str) -> Result Result bearer.to_authorization_header_value(), None => { - warn!("No JWT bearer for getting image gen status!"); - return Err(SoraError::NoBearerTokenAvailable) + warn!("No JWT bearer token in client - cannot fetch image gen status!"); + return Err(SoraClientError::NoBearerTokenAvailable.into()); }, }; let client = Client::new(); let mut url = Url::parse(SORA_STATUS_URL) - .map_err(|err| anyhow!("error parsing URL: {:?}", err))?; + .map_err(|err| { + SoraClientError::UrlParseError(err) + })?; // Add query parameters if let Some(limit) = status_request.limit { @@ -232,13 +236,26 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: & .header("Authorization", bearer_header) .header("Content-Type", "application/json"); - let http_request = http_request.build()?; + let http_request = http_request.build() + .map_err(|err| { + SoraClientError::WreqClientError(err) + })?; - let response = client.execute(http_request).await?; + let response = client.execute(http_request) + .await + .map_err(|err| { + error!("Client failed to fetch sora image gen status: {:?}", err); + SoraGenericApiError::WreqError(err) + })?; let status = response.status(); - let response_body = &response.text().await?; + let response_body = &response.text() + .await + .map_err(|err| { + error!("Client failed to read sora image gen status: {:?}", err); + SoraGenericApiError::WreqError(err) + })?; debug!("response_body: {}", response_body); @@ -254,7 +271,8 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: & Ok(response) => Ok(response), Err(err) => { warn!("Failed to parse status response: {:?}", err); - Err(SoraError::JsonErrorWithBody(err, response_body.to_string())) + Err(SoraGenericApiError::SerdeResponseParseErrorWithBody( + err, response_body.to_string()).into()) } } } @@ -316,9 +334,9 @@ mod tests { let bearer = bearer.trim().to_string(); let creds = SoraCredentialBuilder::new() - .cookies(&cookie) - .jwt_bearer_token(&bearer) - .sora_sentinel(&sentinel) + .with_cookies(&cookie) + .with_jwt_bearer_token(&bearer) + .with_sora_sentinel(&sentinel) .build()?; // Get the task status for a specific task diff --git a/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_remix.rs b/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_remix.rs index c22b0bb7ac..81026c9d17 100644 --- a/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_remix.rs +++ b/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_remix.rs @@ -70,9 +70,9 @@ mod tests { let bearer = bearer.trim().to_string(); let creds = SoraCredentialBuilder::new() - .cookies(&cookie) - .jwt_bearer_token(&bearer) - .sora_sentinel(&sentinel) + .with_cookies(&cookie) + .with_jwt_bearer_token(&bearer) + .with_sora_sentinel(&sentinel) .build()?; let response = sora_image_gen_remix(SoraImageGenRemixRequest { diff --git a/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_simple.rs b/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_simple.rs index 70f19c6897..42ed22da78 100644 --- a/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_simple.rs +++ b/crates/api_clients/openai_sora_client/src/requests/image_gen/sora_image_gen_simple.rs @@ -54,9 +54,9 @@ mod tests { let bearer = bearer.trim().to_string(); let creds = SoraCredentialBuilder::new() - .cookies(&cookie) - .jwt_bearer_token(&bearer) - .sora_sentinel(&sentinel) + .with_cookies(&cookie) + .with_jwt_bearer_token(&bearer) + .with_sora_sentinel(&sentinel) .build()?; let response = sora_image_gen_simple(SoraImageGenSimpleRequest { diff --git a/crates/api_clients/openai_sora_client/src/requests/job/sora_job_status.rs b/crates/api_clients/openai_sora_client/src/requests/job/sora_job_status.rs index 0f62870625..ac7fb97b30 100644 --- a/crates/api_clients/openai_sora_client/src/requests/job/sora_job_status.rs +++ b/crates/api_clients/openai_sora_client/src/requests/job/sora_job_status.rs @@ -1,5 +1,8 @@ use crate::creds::sora_credential_set::SoraCredentialSet; -use crate::sora_error::SoraError; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use log::error; use wreq::Client; const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"; @@ -12,12 +15,10 @@ pub async fn sora_job_status( ) -> Result<(), SoraError> { let url = format!("https://sora.com/backend/notif?limit=100&before={}", task_id); - let cookie = credentials.cookies.to_string(); - // TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently. let authorization_header = credentials.jwt_bearer_token.as_ref() - .ok_or(SoraError::NoBearerTokenAvailable)? + .ok_or(SoraClientError::NoBearerTokenAvailable)? .to_authorization_header_value(); let client = Client::new(); @@ -28,9 +29,18 @@ pub async fn sora_job_status( .header("User-Agent", USER_AGENT) .header("Cookie", &cookie); - let response = request.send().await?; + let response = request.send() + .await + .map_err(|err| { + error!("sora fetch job status error: {}", err); + SoraGenericApiError::WreqError(err) + })?; - let json_response = &response.text().await?; + let json_response = &response.text().await + .map_err(|err| { + error!("sora read job status error: {}", err); + SoraGenericApiError::WreqError(err) + })?; //let response = serde_json::from_str(json_response)?; @@ -58,9 +68,9 @@ mod tests { let bearer = bearer.trim().to_string(); let creds = SoraCredentialBuilder::new() - .cookies(&cookie) - .jwt_bearer_token(&bearer) - .sora_sentinel(&sentinel) + .with_cookies(&cookie) + .with_jwt_bearer_token(&bearer) + .with_sora_sentinel(&sentinel) .build()?; let task_id = "task_01jqwwrkvgeqp8jsf5mqk1jceh"; diff --git a/crates/api_clients/openai_sora_client/src/requests/list_media/list_media.rs b/crates/api_clients/openai_sora_client/src/requests/list_media/list_media.rs index 7927e0f0e5..6135d76589 100644 --- a/crates/api_clients/openai_sora_client/src/requests/list_media/list_media.rs +++ b/crates/api_clients/openai_sora_client/src/requests/list_media/list_media.rs @@ -1,6 +1,9 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::requests::image_gen::image_gen_status::VideoGenStatusResponse; use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse; -use crate::sora_error::SoraError; use crate::utils::classify_general_http_error::classify_general_http_error; use log::{error, info}; use once_cell::sync::Lazy; @@ -39,13 +42,17 @@ pub async fn list_media(credentials: &SoraCredentialSet) -> Result Result Result().await?; - Ok(typed_response) + let text_body = &response.text().await + .map_err(|err| { + error!("sora error reading media list text body: {}", err); + SoraGenericApiError::WreqError(err) + })?; + + let response = serde_json::from_str::(text_body) + .map_err(|err| { + error!("Failed to parse media list response: {}", err); + SoraGenericApiError::SerdeResponseParseErrorWithBody(err, text_body.to_string()) + })?; + + Ok(response) } diff --git a/crates/api_clients/openai_sora_client/src/requests/sentinel_refresh/generate/token.rs b/crates/api_clients/openai_sora_client/src/requests/sentinel_refresh/generate/token.rs index 6629b1baaa..da03f238db 100644 --- a/crates/api_clients/openai_sora_client/src/requests/sentinel_refresh/generate/token.rs +++ b/crates/api_clients/openai_sora_client/src/requests/sentinel_refresh/generate/token.rs @@ -1,5 +1,7 @@ use super::request::GenerateSentinelRefreshRequest; -use crate::sora_error::SoraError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::requests::list_media::list_media::ListMediaResponse; use crate::utils::classify_general_http_error::classify_general_http_error; use errors::AnyhowResult; use idempotency::uuid::generate_random_uuid; @@ -49,11 +51,15 @@ pub async fn generate_token() -> Result { let request = SentinelRequest::new(base64_request); let client = Client::new(); let response = client.post(SORA_IMAGE_GEN_URL) - .header("User-Agent", USER_AGENT) - .header("Content-Type", "application/json") - .json(&request) - .send() - .await?; + .header("User-Agent", USER_AGENT) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await + .map_err(|err| { + error!("Client failed to generate sentinel token: {:?}", err); + SoraGenericApiError::WreqError(err) + })?; if !response.status().is_success() { error!("Failed to generate sentinel: {}", response.status()); @@ -61,8 +67,19 @@ pub async fn generate_token() -> Result { return Err(error); } - let response_json: SentinelResponse = response.json().await?; - Ok(response_json.token) + let text_body = &response.text().await + .map_err(|err| { + error!("sora error reading sentinel token text body: {}", err); + SoraGenericApiError::WreqError(err) + })?; + + let response = serde_json::from_str::(&text_body) + .map_err(|err| { + error!("Failed to parse media list response: {}", err); + SoraGenericApiError::SerdeResponseParseErrorWithBody(err, text_body.to_string()) + })?; + + Ok(response.token) } diff --git a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_bytes.rs b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_bytes.rs index 6969ea8813..3703cda94f 100644 --- a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_bytes.rs +++ b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_bytes.rs @@ -1,6 +1,6 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_error::SoraError; use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse}; -use crate::sora_error::SoraError; use std::path::PathBuf; use std::time::Duration; diff --git a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_file.rs b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_file.rs index ad0e5a8247..0b1daa63c5 100644 --- a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_file.rs +++ b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_from_file.rs @@ -1,6 +1,8 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse}; -use crate::sora_error::SoraError; +use log::error; use std::path::Path; use std::time::Duration; use tokio::fs::File; @@ -15,17 +17,29 @@ pub async fn sora_media_upload_from_file>( creds: &SoraCredentialSet, maybe_timeout: Option, ) -> Result { - let mut file = File::open(&file_path).await?; + let mut file = File::open(&file_path).await + .map_err(|err| { + error!("Failed to open file for upload: {}", err); + SoraClientError::FileForUploadReadError(err) + })?; + let mut buffer = Vec::with_capacity(INITIAL_BUFFER_SIZE); - file.read_to_end(&mut buffer).await?; + + file.read_to_end(&mut buffer).await + .map_err(|err| { + error!("Failed to read file for upload: {}", err); + SoraClientError::FileForUploadReadError(err) + })?; let filename = file_path.as_ref().file_name() - .ok_or_else(|| anyhow::anyhow!("Invalid file path"))? + .ok_or_else(|| SoraClientError::FileForUploadHasInvalidPath)? .to_string_lossy() .to_string(); + + let maybe_ext = file_path.as_ref().extension().and_then(|e| e.to_str()); // TODO: Read file magic bytes first, then fall back to this. - let mime_type = match file_path.as_ref().extension().and_then(|e| e.to_str()) { + let mime_type = match maybe_ext { Some("jpg") | Some("jpeg") => "image/jpeg", Some("png") => "image/png", // Some("webp") => "image/webp", @@ -59,8 +73,8 @@ mod tests { let image_path = test_file_path("test_data/image/juno.jpg")?; // media_01jqyqgqpwf40tkcapq5bmaz5d let creds = SoraCredentialBuilder::new() - .cookies(&cookie) - .jwt_bearer_token(&bearer) + .with_cookies(&cookie) + .with_jwt_bearer_token(&bearer) .build()?; let response = sora_media_upload_from_file( diff --git a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_http_request.rs b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_http_request.rs index 5722f2b1c3..37a685c64a 100644 --- a/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_http_request.rs +++ b/crates/api_clients/openai_sora_client/src/requests/upload/upload_media_http_request.rs @@ -1,9 +1,13 @@ use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::requests::bearer::generate_bearer_with_cookie::SoraAuthResponse; use crate::requests::image_gen::image_gen_status::USER_AGENT; -use crate::sora_error::SoraError; use crate::utils::classify_general_http_error::classify_general_http_error; -use log::info; +use log::{error, info}; use serde::Deserialize; +use std::io::empty; use std::time::Duration; use wreq::multipart::{Form, Part}; use wreq::Client; @@ -74,16 +78,17 @@ pub (crate) async fn upload_media_http_request( ) -> Result { // Create multipart form - let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes. - .file_name(filename) // NB: Reqwest needs to own the bytes - .mime_str(mime_type)?; + let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes. + .file_name(filename) // NB: Reqwest needs to own the bytes + .mime_str(mime_type) + .map_err(|e| SoraClientError::MultipartFormError(e))?; let form = Form::new().part("file", part); let cookie = credentials.cookies.to_string(); let auth_header = credentials.jwt_bearer_token .as_ref() - .ok_or(SoraError::NoBearerTokenAvailable)? + .ok_or(SoraClientError::NoBearerTokenAvailable)? .to_authorization_header_value(); // Make API request @@ -98,7 +103,11 @@ pub (crate) async fn upload_media_http_request( request_builder = request_builder.timeout(timeout); } - let response = request_builder.send().await?; + let response = request_builder.send() + .await + .map_err(|err| { + SoraGenericApiError::WreqError(err) + })?; // Check response status if !response.status().is_success() { @@ -107,7 +116,17 @@ pub (crate) async fn upload_media_http_request( return Err(error); } - // Parse response - let upload_response = response.json::().await?; + let response_body = &response.text().await + .map_err(|err| { + error!("Error reading response body while attempting file upload: {:?}", err); + SoraGenericApiError::WreqError(err) + })?; + + let upload_response : SoraMediaUploadResponse = serde_json::from_str(&response_body) + .map_err(|err| { + error!("Error parsing response body while attempting file upload: {:?}", err); + SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()) + })?; + Ok(upload_response) } diff --git a/crates/api_clients/openai_sora_client/src/sora_error.rs b/crates/api_clients/openai_sora_client/src/sora_error.rs deleted file mode 100644 index ace3f603e8..0000000000 --- a/crates/api_clients/openai_sora_client/src/sora_error.rs +++ /dev/null @@ -1,166 +0,0 @@ -use cloudflare_errors::cloudflare_error::CloudflareError; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::io; - -/// The single error type this crate should surface to all callers. -#[derive(Debug)] -pub enum SoraError { - /// We haven't received a bearer token yet - /// This is our own internal application state error, not something Sora returns. - NoBearerTokenAvailable, - - /// We're sending too many tasks to Sora. - TooManyConcurrentTasks, - - // TODO: Try to make concrete error types for these. - /// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login. - /// - /// Example message, e.g. from the upload endpoint: - /// { - /// "error": { - /// "message": "Your authentication token has expired. Please try signing in again.", - /// "type": "invalid_request_error", - /// "param": null, - /// "code": "token_expired" - /// } - /// } - UnauthorizedCookieOrBearerExpired, - - /// Failed to generate a bearer token despite the `200 OK` response. - /// This has been generated by sending old/expired session cookies or empty cookies. - FailedToGenerateBearer, - - /// Sora requires users to set up an account username before they can generate images. - /// This requires the user to set up that username. - SoraUsernameNotYetCreated, - - /// Something is wrong with the JWT bearer token. - /// This error originates on our end as we try to parse the JWT. - LocalJwtClaimsParseError(String), - - /// Sora is having issues. - /// Typically served from Cloudflare - /// We preserve the message for debugging. - BadGateway(String), - - /// Specific Cloudflare errors. - CloudflareError(CloudflareError), - - /// Another error occurred. - OtherBadStatus(anyhow::Error), - - /// Reqwest Error, eg. connection failures. - WreqError(wreq::Error), - - /// std::io Error that arises from our end, eg. reading from the filesystem. - IoError(io::Error), - - /// anyhow::Error arises from our end - AnyhowError(anyhow::Error), - - /// Issue with setting credentials - SoraCredentialBuilderError(String), - - /// serde_json::Error, likely from JSON deserialization schema mismatch. - JsonError(serde_json::Error), - - /// serde_json::Error, likely from JSON deserialization schema mismatch. - /// Includes the original body. - JsonErrorWithBody(serde_json::Error, String), -} - -impl SoraError { - pub fn is_sora_having_downtime_issues(&self) -> bool { - match self { - Self::BadGateway(_) => true, - Self::CloudflareError(CloudflareError::GatewayTimeout504) => true, - Self::CloudflareError(CloudflareError::TimeoutOccurred524) => true, - _ => false, - } - } -} - -impl Error for SoraError {} - -impl Display for SoraError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::NoBearerTokenAvailable => { - write!(f, "Unauthorized: no bearer token set") - } - Self::FailedToGenerateBearer => { - write!(f, "Failed to generate bearer. Probably invalid session.") - } - Self::SoraUsernameNotYetCreated => { - write!(f, "Sora username not created. Please set up a username.") - } - Self::LocalJwtClaimsParseError(message) => { - write!(f, "Local JWT claims parse error: {}", message) - } - Self::TooManyConcurrentTasks => { - write!(f, "Too many concurrent tasks. Please wait.") - } - Self::UnauthorizedCookieOrBearerExpired => { - write!(f, "Unauthorized: cookie and/or bearer token expired") - } - Self::BadGateway(message) => { - write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message) - } - Self::CloudflareError(error) => { - write!(f, "Cloudflare error: {}", error) - } - Self::OtherBadStatus(err) => { - write!(f, "Other error: {}", err) - } - Self::WreqError(err) => { - write!(f, "Wreq error: {}", err) - } - Self::IoError(err) => { - write!(f, "IO error: {}", err) - } - Self::AnyhowError(err) => { - write!(f, "Anyhow error: {}", err) - } - Self::SoraCredentialBuilderError(message) => { - write!(f, "Sora credential builder error: {}", message) - } - Self::JsonError(err) => { - write!(f, "serde_json error: {}", err) - } - Self::JsonErrorWithBody(err, message) => { - write!(f, "serde_json error: {:?} body: {:?}", err, message) - } - } - } -} - -impl From for SoraError { - fn from(err: wreq::Error) -> SoraError { - Self::WreqError(err) - } -} - -impl From for SoraError { - fn from(err: io::Error) -> Self { - Self::IoError(err) - } -} - -impl From for SoraError { - fn from(err: anyhow::Error) -> Self { - Self::AnyhowError(err) - } -} - -impl From for SoraError { - fn from(err: serde_json::Error) -> Self { - Self::JsonError(err) - } -} - -impl From for SoraError { - fn from(err: CloudflareError) -> Self { - Self::CloudflareError(err) - } -} diff --git a/crates/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs b/crates/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs index 28fe1ec9a6..797437f41f 100644 --- a/crates/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs +++ b/crates/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs @@ -1,4 +1,5 @@ -use crate::sora_error::SoraError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; use crate::utils::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body; /// This assumes the request failed and returned a non-200. @@ -7,7 +8,7 @@ pub async fn classify_general_http_error(response: wreq::Response) -> SoraError let status = response.status(); let message = match response.text().await { Ok(text) => text, - Err(err) => return SoraError::WreqError(err), + Err(err) => return SoraGenericApiError::WreqError(err).into(), }; classify_general_http_status_code_and_body(status, &message).await diff --git a/crates/api_clients/openai_sora_client/src/utils/classify_general_http_status_code_and_body.rs b/crates/api_clients/openai_sora_client/src/utils/classify_general_http_status_code_and_body.rs index 7b344bc558..02b37ffab7 100644 --- a/crates/api_clients/openai_sora_client/src/utils/classify_general_http_status_code_and_body.rs +++ b/crates/api_clients/openai_sora_client/src/utils/classify_general_http_status_code_and_body.rs @@ -1,4 +1,6 @@ -use crate::sora_error::SoraError; +use crate::error::sora_error::SoraError; +use crate::error::sora_generic_api_error::SoraGenericApiError; +use crate::error::sora_specific_api_error::SoraSpecificApiError; use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors; use wreq::StatusCode; /* @@ -38,7 +40,7 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp || message.contains("token_expired"); if cookie_expired { - return SoraError::UnauthorizedCookieOrBearerExpired; + return SoraSpecificApiError::UnauthorizedCookieOrBearerExpired.into(); } let needs_onboarding = @@ -46,21 +48,24 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp || message.contains("onboarding_required"); if needs_onboarding { - return SoraError::SoraUsernameNotYetCreated; + return SoraSpecificApiError::SoraUsernameNotYetCreated.into(); } let status_code = status.as_u16(); if let Err(err) = filter_cloudflare_errors(status_code, &response_body) { - return SoraError::CloudflareError(err); + return SoraGenericApiError::CloudflareError(err).into(); } match status_code { 502 => { - return SoraError::BadGateway(message); + return SoraGenericApiError::Http502ErrorBadGateway(message.to_string()).into(); } _ => {}, // Fall-through } - SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message)) + SoraGenericApiError::UncategorizedBadResponseWithStatusAndBody { + status_code: status, + body: response_body.to_string(), + }.into() } diff --git a/crates/api_clients/openai_sora_client/src/utils/lightweight_sora_jwt_parse.rs b/crates/api_clients/openai_sora_client/src/utils/lightweight_sora_jwt_parse.rs index d0e82572e2..a393500b61 100644 --- a/crates/api_clients/openai_sora_client/src/utils/lightweight_sora_jwt_parse.rs +++ b/crates/api_clients/openai_sora_client/src/utils/lightweight_sora_jwt_parse.rs @@ -1,7 +1,7 @@ //! Adapted from 'jwt' crate -use crate::sora_error::SoraError; -use anyhow::anyhow; +use crate::error::sora_client_error::SoraClientError; +use crate::error::sora_error::SoraError; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use chrono::{DateTime, Utc}; @@ -24,11 +24,10 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> Result = serde_json::from_str(&claims) - .map_err(|err| SoraError::LocalJwtClaimsParseError( + .map_err(|err| as_jwt_error( format!("failure to parse claims json: {:?}", err)))?; let iat = claims.get("iat") @@ -110,7 +109,11 @@ fn decode_base64(raw: &str) -> AnyhowResult { } fn jwt_error(reason: &str) -> SoraError { - SoraError::LocalJwtClaimsParseError(reason.to_string()) + SoraError::Client(SoraClientError::LocalJwtClaimsParseError(reason.to_string())) +} + +fn as_jwt_error(reason: String) -> SoraError { + SoraError::Client(SoraClientError::LocalJwtClaimsParseError(reason)) } #[cfg(test)] diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs index 782591e027..d9fc4d47da 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs @@ -3,7 +3,7 @@ use base64::DecodeError; use errors::AnyhowError; use fal_client::error::fal_error_plus::FalErrorPlus; use midjourney_client::error::midjourney_error::MidjourneyError; -use openai_sora_client::sora_error::SoraError; +use openai_sora_client::error::sora_error::SoraError; use storyteller_client::error::storyteller_error::StorytellerError; #[derive(Debug)] diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs index d5c967ddf2..657ecc2bbd 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs @@ -32,7 +32,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; -use openai_sora_client::sora_error::SoraError; use serde_derive::{Deserialize, Serialize}; use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs}; use std::time::Duration; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs index 05d0f4fbd5..d00e72e1a8 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs @@ -35,7 +35,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; -use openai_sora_client::sora_error::SoraError; use serde_derive::{Deserialize, Serialize}; use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs}; use std::time::Duration; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs index 36f9e96cd0..f6581ee35b 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/gpt_image_1/handle_gpt_image_1_edit_sora.rs @@ -34,7 +34,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; -use openai_sora_client::sora_error::SoraError; use std::time::Duration; use storyteller_client::endpoints::media_files::get_media_file::get_media_file; use tauri::AppHandle; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs index c9edf96b9a..1b265a9947 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs @@ -37,7 +37,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; -use openai_sora_client::sora_error::SoraError; use serde_derive::{Deserialize, Serialize}; use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs}; use std::time::Duration; diff --git a/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs index 981425ffef..8fcd229c01 100644 --- a/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs @@ -14,15 +14,16 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager use crate::services::sora::state::sora_task_queue::SoraTaskQueue; use errors::AnyhowError; use log::{error, info, warn}; +use openai_sora_client::error::sora_error::SoraError; +use openai_sora_client::error::sora_specific_api_error::SoraSpecificApiError; use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest}; use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest}; use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages}; -use openai_sora_client::sora_error::SoraError; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; -use storyteller_client::error::storyteller_error::StorytellerError; use storyteller_client::endpoints::media_files::get_media_file::get_media_file; +use storyteller_client::error::storyteller_error::StorytellerError; use tauri::{AppHandle, Manager, State}; use tokens::tokens::media_files::MediaFileToken; @@ -135,12 +136,12 @@ pub async fn sora_image_remix_command( let mut error_message = "An error occurred. Please try again. If it continues, please tell our staff about the problem."; match (err) { - InnerError::SoraError(SoraError::TooManyConcurrentTasks) => { + InnerError::SoraError(SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks)) => { error_type = SoraImageRemixErrorType::TooManyConcurrentTasks; status = CommandErrorStatus::ServerError; error_message = "You have too many Sora image generation tasks running. Please wait a moment."; }, - InnerError::SoraError(SoraError::SoraUsernameNotYetCreated) => { + InnerError::SoraError(SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated)) => { error_type = SoraImageRemixErrorType::SoraUsernameNotYetCreated; status = CommandErrorStatus::BadRequest; error_message = "Your Sora username is not yet created. Please visit the Sora.com website to create it first."; diff --git a/crates/lib/shared_service_components/src/sora_redis_credentials/get_sora_credentials_from_redis.rs b/crates/lib/shared_service_components/src/sora_redis_credentials/get_sora_credentials_from_redis.rs index d1e0c834fe..2c43410a17 100644 --- a/crates/lib/shared_service_components/src/sora_redis_credentials/get_sora_credentials_from_redis.rs +++ b/crates/lib/shared_service_components/src/sora_redis_credentials/get_sora_credentials_from_redis.rs @@ -22,9 +22,9 @@ pub fn get_sora_credentials_from_redis( match (bearer, cookie, sentinel) { (Some(b), Some(c), Some(s)) => { let creds = SoraCredentialBuilder::new() - .jwt_bearer_token(&b) - .cookies(&c) - .sora_sentinel(&s) + .with_jwt_bearer_token(&b) + .with_cookies(&c) + .with_sora_sentinel(&s) .build()?; Ok(creds) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/get_sora_credentials_from_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/get_sora_credentials_from_request.rs index 3f91bf8b76..71ac6a2093 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/get_sora_credentials_from_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/image_studio/get_sora_credentials_from_request.rs @@ -27,9 +27,9 @@ pub fn get_sora_credentials_from_request( info!("Sora credentials were present in HTTP request headers!"); let mut creds = SoraCredentialBuilder::new() - .cookies(cookie) - .jwt_bearer_token(bearer) - .sora_sentinel(sentinel) + .with_cookies(cookie) + .with_jwt_bearer_token(bearer) + .with_sora_sentinel(sentinel) .build()?; return Ok(creds); @@ -44,15 +44,15 @@ pub fn get_sora_credentials_from_request( let mut builder = credentials.to_builder(); if let Some(bearer) = sora_bearer { info!("override bearer value"); - builder = builder.jwt_bearer_token(&bearer); + builder = builder.with_jwt_bearer_token(&bearer); } if let Some(cookie) = sora_cookie { info!("override cookie value"); - builder = builder.cookies(&cookie) + builder = builder.with_cookies(&cookie) } if let Some(sentinel) = sora_sentinel { info!("override sentinel value"); - builder = builder.sora_sentinel(&sentinel); + builder = builder.with_sora_sentinel(&sentinel); } return Ok(builder.build()?); },