get rid of legacy error type

This commit is contained in:
Brandon Thomas committed 2025-10-01 23:15:30 -04:00
1 parent 2fbd2c8f8b
commit 2cc2af83c7
13 files changed
+130 -94

No files matched your search

@@ -19,7 +19,10 @@ pub enum SoraClientError {
/// 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,
NoBearerTokenForRequest,
/// A sentinel token is not present in the client, which is required for some requests.
NoSentinelTokenForRequest,
/// Issue with using the SoraCredentialBuilder.
SoraCredentialBuilderError(&'static str),
@@ -40,7 +43,8 @@ impl Display for SoraClientError {
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::NoBearerTokenForRequest => write!(f, "No bearer token available. The client needs a bearer token to make the request."),
Self::NoSentinelTokenForRequest => write!(f, "No sentinel token available. The client needs a sentinel 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),
@@ -18,6 +18,11 @@ pub enum SoraGenericApiError {
/// Includes the original body.
SerdeResponseParseErrorWithBody(serde_json::Error, String),
/// serde_json::Error, likely from JSON deserialization schema mismatch.
/// Includes the original body.
/// Specifically, this is for non-200 (error) responses.
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
/// Another error occurred.
/// TODO: Try to get rid of this.
UncategorizedBadResponse(String),
@@ -40,6 +45,7 @@ impl Display for SoraGenericApiError {
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::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 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),
@@ -7,10 +7,21 @@ pub enum SoraSpecificApiError {
/// This has been generated by sending old/expired session cookies or empty cookies.
FailedToGenerateBearerToken,
/// The endpoint told us we used an invalid JWT.
InvalidJwt,
/// Sora has responded with "sentinel block".
/// This usually means we need to refresh our sentinel token.
SentinelBlockError,
/// Sora requires users to set up an account username before they can generate images.
/// This requires the user to set up that username.
SoraUsernameNotYetCreated,
/// Sora has responded with "token expired".
/// This (probably?) means we need to refresh our JWT bearer token.
TokenExpiredError,
/// We're sending too many tasks to Sora.
/// Sora is telling us to cool it.
TooManyConcurrentTasks,
@@ -36,7 +47,10 @@ 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::InvalidJwt => write!(f, "Invalid JWT token. Please refresh the token."),
Self::SentinelBlockError => write!(f, "Request blocked by Sentinel Token Block. Please refresh the sentinel token."),
Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."),
Self::TokenExpiredError => write!(f, "JWT token has expired. Please refresh the token."),
Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."),
Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"),
}
@@ -11,7 +11,7 @@ use crate::requests::image_gen::{image_gen_http_request, SoraImageGenError};
use crate::requests::sentinel_refresh::generate::token::generate_token;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{info, warn};
use log::{error, info, warn};
use std::time::Duration;
pub struct ImageRemixAutoRenewRequest<'a> {
@@ -47,36 +47,41 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
let mut refresh_sentinel = false;
match err {
// We'll fail these requests...
SoraImageGenError::TooManyConcurrentTasks(_) => {
return Err(SoraSpecificApiError::TooManyConcurrentTasks.into());
}
SoraImageGenError::UsernameRequired(_err) => {
return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into())
}
SoraImageGenError::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(SoraGenericApiError::UncategorizedBadResponse(format!("network error: {:?}", err)).into())
// We'll specifically fail these non-retryable requests...
SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks) |
SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated) => {
error!("Non-retryable image generation error: {:?}", err);
return Err(err);
}
// We'll retry these requests...
SoraImageGenError::SentinelBlock(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
warn!("Image generation failed due to sentinel block error: {:?}", err);
refresh_sentinel = true;
}
SoraImageGenError::TokenExpired(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
warn!("Image generation failed due to token expired error: {:?}", err);
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
refresh_jwt = true;
}
SoraImageGenError::InvalidJwt(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
warn!("Image generation failed due to invalid jwt error: {:?}", err);
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
warn!("Image generation failed due to auth error (nb: this is a legacy error): {:?}", err);
refresh_jwt = true;
refresh_sentinel = true;
}
// We'll fail everything else eagerly...
_ => {
error!("Image generation error: {:?}", err);
return Err(err);
}
}
let mut new_bearer = None;
@@ -132,18 +137,7 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
sora_media_tokens: request.sora_media_tokens,
credentials: &new_creds,
request_timeout: request.request_timeout,
}).await;
match result {
Ok(response) => Ok((response, Some(new_creds))),
Err(err) => match err {
SoraImageGenError::TooManyConcurrentTasks(err) => {
Err(SoraSpecificApiError::TooManyConcurrentTasks.into())
}
_ => {
warn!("Image remix failed again: {:?}", err);
Err(SoraGenericApiError::UncategorizedBadResponse(format!("image remix failed again: {:?}", err)).into())
}
}
}
}).await?;
Ok((result, Some(new_creds)))
}
@@ -12,7 +12,7 @@ use log::{error, info, warn};
pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, error: SoraError) -> Result<SoraCredentialSet, SoraError> {
match error {
SoraError::Client(SoraClientError::NoBearerTokenAvailable) => {
SoraError::Client(SoraClientError::NoBearerTokenForRequest) => {
error!("Previous request failed due to missing bearer token.");
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
@@ -46,36 +46,41 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
let mut refresh_sentinel = false;
match err {
// We'll fail these requests...
SoraImageGenError::TooManyConcurrentTasks(_) => {
return Err(SoraSpecificApiError::TooManyConcurrentTasks.into());
// We'll specifically fail these non-retryable requests...
SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks) |
SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated) => {
error!("Non-retryable image generation error: {:?}", err);
return Err(err);
}
SoraImageGenError::UsernameRequired(_err) => {
return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into())
}
SoraImageGenError::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(SoraGenericApiError::UncategorizedBadResponse(format!("network error (todo: this should be a reqwest error): {:?}", err)).into())
}
// We'll retry these requests...
SoraImageGenError::SentinelBlock(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
warn!("Image generation failed due to sentinel block error: {:?}", err);
refresh_sentinel = true;
}
SoraImageGenError::TokenExpired(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
warn!("Image generation failed due to token expired error: {:?}", err);
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
refresh_jwt = true;
}
SoraImageGenError::InvalidJwt(err) => {
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
warn!("Image generation failed due to invalid jwt error: {:?}", err);
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
warn!("Image generation failed due to auth error (nb: this is a legacy error): {:?}", err);
refresh_jwt = true;
refresh_sentinel = true;
}
// We'll fail everything else eagerly...
_ => {
error!("Image generation error: {:?}", err);
return Err(err);
}
}
let mut new_bearer = None;
@@ -133,18 +138,7 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
image_size: request.image_size,
credentials: &new_creds,
request_timeout: request.request_timeout,
}).await;
match result {
Ok(response) => Ok((response, Some(new_creds))),
Err(err) => match err {
image_gen_http_request::SoraImageGenError::TooManyConcurrentTasks(err) => {
Err(SoraSpecificApiError::TooManyConcurrentTasks.into())
}
_ => {
warn!("Image gen failed again: {:?}", err);
Err(SoraGenericApiError::UncategorizedBadResponse(format!("image gen failed again: {:?}", err)).into())
}
}
}
}).await?;
Ok((result, Some(new_creds)))
}
@@ -1,7 +1,11 @@
use crate::constants::user_agent::USER_AGENT;
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::error::sora_specific_api_error::SoraSpecificApiError;
use crate::requests::image_gen::image_gen_status::TaskId;
use log::warn;
use log::{error, warn};
use serde_derive::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
@@ -156,20 +160,18 @@ pub (crate) async fn image_gen_http_request(
sora_request: RawSoraImageGenRequest,
credentials: &SoraCredentialSet,
request_timeout: Option<Duration>,
) -> Result<RawSoraResponse, SoraImageGenError> {
) -> Result<RawSoraResponse, SoraError> {
let client = Client::new();
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(SoraImageGenError::InvalidJwt("JWT bearer is required for image generation".to_string()))?
.ok_or(SoraClientError::NoBearerTokenForRequest)?
.to_authorization_header_value();
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
let sentinel = credentials.sora_sentinel.as_ref()
.map(|sentinel| sentinel.get_sentinel().to_string())
.ok_or(SoraImageGenError::SentinelBlock("Sentinel is required for image generation.".to_string()))?;
.ok_or(SoraClientError::NoSentinelTokenForRequest)?;
let mut http_request = client.post(SORA_IMAGE_GEN_URL)
.header("User-Agent", USER_AGENT)
@@ -184,52 +186,70 @@ pub (crate) async fn image_gen_http_request(
}
let http_request = http_request.json(&sora_request).build()
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
.map_err(|err| {
error!("Error building Sora image generation HTTP request: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
let response = client.execute(http_request)
.await
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
.map_err(|err| {
error!("Error during Sora image generation request: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
let status = response.status();
let response_body = &response.text().await
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
.map_err(|err| {
error!("Error reading Sora image generation response body: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
if status != StatusCode::OK {
warn!("Sora image generation failure. Raw response: {:?}", response_body);
let error_response: RawSoraErrorResponse = serde_json::from_str(response_body)
.map_err(|e| SoraImageGenError::GenericError(format!("Failed to parse error response: {}", e)))?;
.map_err(|err| SoraGenericApiError::SerdeParseErrorWithBodyOnNon200(err, response_body.to_string()))?;
// Check for specific error codes
if let Some(code) = &error_response.error.code {
match code {
SoraErrorCode::TokenExpired => {
return Err(SoraImageGenError::TokenExpired(error_response.error.message));
error!("Sora token expired: {}", error_response.error);
return Err(SoraSpecificApiError::TokenExpiredError.into());
}
SoraErrorCode::SentinelBlock => {
return Err(SoraImageGenError::SentinelBlock(error_response.error.message));
error!("Sora image generation error sentinel block: {}", error_response.error);
return Err(SoraSpecificApiError::SentinelBlockError.into());
}
SoraErrorCode::TooManyConcurrentTasks => {
return Err(SoraImageGenError::TooManyConcurrentTasks(error_response.error.message));
error!("Too many concurrent tasks. Please wait.");
return Err(SoraSpecificApiError::TooManyConcurrentTasks.into());
}
SoraErrorCode::Unknown(_) => {
return Err(SoraImageGenError::GenericError(error_response.error.message));
SoraErrorCode::Unknown(message) => {
error!("Unknown error: {} - message: {}", error_response.error.message, message);
return Err(SoraGenericApiError::UncategorizedBadResponse(error_response.error.message).into());
}
SoraErrorCode::InvalidJwt => {
return Err(SoraImageGenError::InvalidJwt(error_response.error.message));
error!("Invalid JWT token: {}", error_response.error.message);
return Err(SoraSpecificApiError::InvalidJwt.into());
}
SoraErrorCode::UsernameRequired => {
return Err(SoraImageGenError::UsernameRequired(error_response.error.message));
error!("Username required: {}", error_response.error.message);
return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into());
}
}
}
return Err(SoraImageGenError::GenericError(error_response.error.message));
error!("Sora image generation failure: {}", error_response.error);
return Err(SoraGenericApiError::UncategorizedBadResponse(
format!("Unknown error code: {:?}, message: {:?}", error_response.error.code, error_response.error.message))
.into());
}
let response = serde_json::from_str(response_body)
.map_err(|e| SoraImageGenError::GenericError(format!("Failed to parse success response: {}", e)))?;
.map_err(|err| SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
Ok(response)
}
@@ -208,7 +208,7 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
Some(bearer) => bearer.to_authorization_header_value(),
None => {
warn!("No JWT bearer token in client - cannot fetch image gen status!");
return Err(SoraClientError::NoBearerTokenAvailable.into());
return Err(SoraClientError::NoBearerTokenForRequest.into());
},
};
@@ -1,4 +1,5 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::error::sora_error::SoraError;
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, InpaintItem, InpaintItemType, OperationType, RawSoraImageGenRequest, SoraImageGenError, VideoGenType};
use log::info;
@@ -15,7 +16,7 @@ pub struct SoraImageGenRemixRequest<'a> {
/// The "remix" commands let you supply additional images as context.
/// Sora "media tokens" of previously uploaded images must be supplied.
pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Result<SoraImageGenResponse, SoraImageGenError> {
pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Result<SoraImageGenResponse, SoraError> {
let args = RawSoraImageGenRequest {
r#type: VideoGenType::ImageGen,
operation: OperationType::Remix,
@@ -1,7 +1,7 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::error::sora_error::SoraError;
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, OperationType, RawSoraImageGenRequest, VideoGenType};
use crate::requests::image_gen::SoraImageGenError;
use std::time::Duration;
pub struct SoraImageGenSimpleRequest<'a> {
@@ -12,7 +12,7 @@ pub struct SoraImageGenSimpleRequest<'a> {
pub request_timeout: Option<Duration>,
}
pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Result<SoraImageGenResponse, SoraImageGenError> {
pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Result<SoraImageGenResponse, SoraError> {
let sora_request = RawSoraImageGenRequest {
r#type: VideoGenType::ImageGen,
operation: OperationType::SimpleCompose,
@@ -24,8 +24,11 @@ pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Resul
inpaint_items: vec![],
};
// TODO: Error handling.
let result = image_gen_http_request(sora_request, args.credentials, args.request_timeout).await?;
let result = image_gen_http_request(
sora_request,
args.credentials,
args.request_timeout
).await?;
Ok(SoraImageGenResponse {
task_id: result.id,
@@ -17,7 +17,7 @@ pub async fn sora_job_status(
let cookie = credentials.cookies.to_string();
let authorization_header = credentials.jwt_bearer_token.as_ref()
.ok_or(SoraClientError::NoBearerTokenAvailable)?
.ok_or(SoraClientError::NoBearerTokenForRequest)?
.to_authorization_header_value();
let client = Client::new();
@@ -42,7 +42,7 @@ pub async fn list_media(credentials: &SoraCredentialSet) -> Result<ListMediaResp
let auth_header = credentials.jwt_bearer_token
.as_ref()
.map(|bearer| bearer.to_authorization_header_value())
.ok_or_else(|| SoraClientError::NoBearerTokenAvailable)?;
.ok_or_else(|| SoraClientError::NoBearerTokenForRequest)?;
let cookie = credentials.cookies.to_string();
@@ -88,7 +88,7 @@ pub (crate) async fn upload_media_http_request(
let cookie = credentials.cookies.to_string();
let auth_header = credentials.jwt_bearer_token
.as_ref()
.ok_or(SoraClientError::NoBearerTokenAvailable)?
.ok_or(SoraClientError::NoBearerTokenForRequest)?
.to_authorization_header_value();
// Make API request