mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 10:39:37 +00:00
new error types
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
pub mod seedance2pro_bad_request_api_error;
|
||||
pub mod seedance2pro_client_error;
|
||||
pub mod seedance2pro_error;
|
||||
pub mod seedance2pro_generic_api_error;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// These represent known "400s" that are the fault of the client sending a bad request.
|
||||
#[derive(Debug)]
|
||||
pub enum Seedance2ProBadRequestApiError {
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation { raw_body: String },
|
||||
|
||||
/// The prompt exceeded Kinovi's maximum length (10,000 characters at the
|
||||
/// time of writing).
|
||||
PromptIsTooLong { raw_body: String },
|
||||
|
||||
/// Too many media URLs were attached to the request (`uploadedUrls` is
|
||||
/// capped at 9 elements at the time of writing).
|
||||
TooManyUrls { raw_body: String },
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProBadRequestApiError {}
|
||||
|
||||
impl Display for Seedance2ProBadRequestApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::VideoGenerationViolation { raw_body } => write!(f, "Video generation violation: {}", raw_body),
|
||||
Self::PromptIsTooLong { raw_body } => write!(f, "Prompt is too long: {}", raw_body),
|
||||
Self::TooManyUrls { raw_body } => write!(f, "Too many urls: {}", raw_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
@@ -9,6 +10,7 @@ pub enum Seedance2ProError {
|
||||
Client(Seedance2ProClientError),
|
||||
ApiSpecific(Seedance2ProSpecificApiError),
|
||||
ApiGeneric(Seedance2ProGenericApiError),
|
||||
ApiBadRequest(Seedance2ProBadRequestApiError),
|
||||
}
|
||||
|
||||
impl Seedance2ProError {
|
||||
@@ -38,6 +40,7 @@ impl std::fmt::Display for Seedance2ProError {
|
||||
Self::Client(e) => write!(f, "Seedance2ProClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "Seedance2ProSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "Seedance2ProGenericApiError: {:?}", e),
|
||||
Self::ApiBadRequest(e) => write!(f, "Seedance2ProBadRequestApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,3 +62,9 @@ impl From<Seedance2ProGenericApiError> for Seedance2ProError {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Seedance2ProBadRequestApiError> for Seedance2ProError {
|
||||
fn from(error: Seedance2ProBadRequestApiError) -> Self {
|
||||
Self::ApiBadRequest(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ pub enum Seedance2ProSpecificApiError {
|
||||
/// The session cookies are expired or invalid.
|
||||
UnauthorizedSessionExpired,
|
||||
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation(String),
|
||||
|
||||
/// The account does not have enough credits to perform the operation.
|
||||
BillingError {
|
||||
status_code: StatusCode,
|
||||
@@ -24,7 +21,6 @@ impl Display for Seedance2ProSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnauthorizedSessionExpired => write!(f, "Unauthorized: session cookies expired or invalid."),
|
||||
Self::VideoGenerationViolation(body) => write!(f, "Video generation violation: {}", body),
|
||||
Self::BillingError { status_code, body } => write!(f, "Billing error (status {}): {}", status_code, body),
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_image::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -293,7 +293,7 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
}
|
||||
|
||||
Ok(GenerateImageResponse {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_video::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -411,7 +411,7 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
}
|
||||
|
||||
Ok(GenerateVideoResponse {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::workflow_run_task::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -423,7 +423,7 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result<Workflow
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
}
|
||||
|
||||
Ok(WorkflowRunTaskResponse {
|
||||
|
||||
+168
-55
@@ -1,5 +1,6 @@
|
||||
use wreq::StatusCode;
|
||||
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
@@ -9,11 +10,25 @@ const BILLING_ERROR_MARKERS: &[&str] = &[
|
||||
"credits not enough",
|
||||
];
|
||||
|
||||
/// Zod validation message for a string field exceeding its maximum length,
|
||||
/// eg. "参数验证失败 - String must contain at most 10000 character(s)".
|
||||
const STRING_TOO_LONG_MARKER: &str = "String must contain at most";
|
||||
|
||||
/// Zod validation message for an array field exceeding its maximum size,
|
||||
/// eg. "参数验证失败 - Array must contain at most 9 element(s)".
|
||||
const ARRAY_TOO_LARGE_MARKER: &str = "Array must contain at most";
|
||||
|
||||
/// Zod `fieldErrors` path entry for the prompt field.
|
||||
const PROMPT_FIELD_MARKER: &str = "\"prompt\"";
|
||||
|
||||
/// Zod `fieldErrors` path entry for the uploaded media urls field.
|
||||
const UPLOADED_URLS_FIELD_MARKER: &str = "\"uploadedUrls\"";
|
||||
|
||||
/// Categorize a non-success HTTP response into a specific or generic error.
|
||||
///
|
||||
/// Checks the response body for known patterns (e.g. billing errors) and returns
|
||||
/// a specific error variant when possible, falling back to
|
||||
/// `UncategorizedBadResponseWithStatusAndBody` otherwise.
|
||||
/// Checks the response body for known patterns (e.g. billing errors, request
|
||||
/// validation failures) and returns a specific error variant when possible,
|
||||
/// falling back to `UncategorizedBadResponseWithStatusAndBody` otherwise.
|
||||
pub fn categorize_seedance2pro_error(
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
@@ -25,6 +40,15 @@ pub fn categorize_seedance2pro_error(
|
||||
}.into();
|
||||
}
|
||||
|
||||
if status_code == StatusCode::BAD_REQUEST {
|
||||
if is_prompt_too_long_error(&body) {
|
||||
return Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: body }.into();
|
||||
}
|
||||
if is_too_many_urls_error(&body) {
|
||||
return Seedance2ProBadRequestApiError::TooManyUrls { raw_body: body }.into();
|
||||
}
|
||||
}
|
||||
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
@@ -37,75 +61,164 @@ fn is_billing_error(body: &str) -> bool {
|
||||
BILLING_ERROR_MARKERS.iter().any(|marker| body_lower.contains(marker))
|
||||
}
|
||||
|
||||
/// Check if the response body is a zod validation failure for an over-long prompt.
|
||||
fn is_prompt_too_long_error(body: &str) -> bool {
|
||||
body.contains(STRING_TOO_LONG_MARKER) && body.contains(PROMPT_FIELD_MARKER)
|
||||
}
|
||||
|
||||
/// Check if the response body is a zod validation failure for too many uploaded urls.
|
||||
fn is_too_many_urls_error(body: &str) -> bool {
|
||||
body.contains(ARRAY_TOO_LARGE_MARKER) && body.contains(UPLOADED_URLS_FIELD_MARKER)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
#[test]
|
||||
fn real_not_enough_credits_response() {
|
||||
let body = std::fs::read_to_string("test_data/responses/not_enough_credits.json")
|
||||
.expect("Failed to read test data file");
|
||||
mod billing_tests {
|
||||
use super::*;
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
#[test]
|
||||
fn real_not_enough_credits_response() {
|
||||
let body = read_response_body("not_enough_credits.json");
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { status_code, body }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("credits not enough"));
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { status_code, body }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("credits not enough"));
|
||||
}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn billing_error_case_insensitive() {
|
||||
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
|
||||
#[test]
|
||||
fn billing_error_case_insensitive() {
|
||||
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { .. }
|
||||
) => {}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_error_falls_back_to_uncategorized() {
|
||||
let body = r#"{"error":"something else went wrong"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
body.clone(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(b, body);
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { .. }
|
||||
) => {}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_body_falls_back_to_uncategorized() {
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::new(),
|
||||
);
|
||||
mod bad_request_tests {
|
||||
use super::*;
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
#[test]
|
||||
fn real_prompt_too_long_response() {
|
||||
let body = read_response_body("prompt_too_long.json");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiBadRequest(
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body }
|
||||
) => {
|
||||
assert!(raw_body.contains("String must contain at most 10000 character(s)"));
|
||||
assert!(raw_body.contains("\"prompt\""));
|
||||
}
|
||||
other => panic!("Expected PromptIsTooLong, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_too_many_urls_response() {
|
||||
let body = read_response_body("too_many_urls.json");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiBadRequest(
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { raw_body }
|
||||
) => {
|
||||
assert!(raw_body.contains("Array must contain at most 9 element(s)"));
|
||||
assert!(raw_body.contains("\"uploadedUrls\""));
|
||||
}
|
||||
other => panic!("Expected TooManyUrls, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_request_markers_require_400_status() {
|
||||
let body = read_response_body("prompt_too_long.json");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::INTERNAL_SERVER_ERROR, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_failure_for_other_field_is_not_categorized() {
|
||||
// Same zod shape, but for a field we don't have a variant for.
|
||||
let body = r#"[{"error":{"json":{"message":"validation failed - String must contain at most 100 character(s)","code":-32600,"data":{"code":"BAD_REQUEST","httpStatus":400,"zodError":{"formErrors":[],"fieldErrors":{"apiParams":[{"code":"custom","message":"String must contain at most 100 character(s)","path":["apiParams","negativePrompt"],"fatal":true}]}}}}}]"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod fallback_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unknown_error_falls_back_to_uncategorized() {
|
||||
let body = r#"{"error":"something else went wrong"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
body.clone(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(b, body);
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_body_falls_back_to_uncategorized() {
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::new(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_response_body(filename: &str) -> String {
|
||||
std::fs::read_to_string(format!("test_data/responses/{}", filename))
|
||||
.expect("Failed to read test data file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"error":{"json":{"message":"[\n {\n \"code\": \"custom\",\n \"message\": \"参数验证失败 - String must contain at most 10000 character(s)\",\n \"path\": [\n \"apiParams\",\n \"prompt\"\n ],\n \"fatal\": true\n }\n]","code":-32600,"data":{"code":"BAD_REQUEST","httpStatus":400,"path":"workflow.runTask","zodError":{"formErrors":[],"fieldErrors":{"apiParams":[{"code":"custom","message":"参数验证失败 - String must contain at most 10000 character(s)","path":["apiParams","prompt"],"fatal":true}]}}},"stack":null},"meta":{"values":{"stack":["undefined"]},"v":1}}}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"error":{"json":{"message":"[\n {\n \"code\": \"custom\",\n \"message\": \"参数验证失败 - Array must contain at most 9 element(s)\",\n \"path\": [\n \"apiParams\",\n \"uploadedUrls\"\n ],\n \"fatal\": true\n }\n]","code":-32600,"data":{"code":"BAD_REQUEST","httpStatus":400,"path":"workflow.runTask","zodError":{"formErrors":[],"fieldErrors":{"apiParams":[{"code":"custom","message":"参数验证失败 - Array must contain at most 9 element(s)","path":["apiParams","uploadedUrls"],"fatal":true}]}}},"stack":null},"meta":{"values":{"stack":["undefined"]},"v":1}}}]
|
||||
Reference in New Issue
Block a user