diff --git a/crates/x-media/src/site/pixiv/api.rs b/crates/x-media/src/site/pixiv/api.rs index 43c8b2a..9cc391b 100644 --- a/crates/x-media/src/site/pixiv/api.rs +++ b/crates/x-media/src/site/pixiv/api.rs @@ -39,6 +39,11 @@ pub enum PixivError { Status(u16), #[error("pixiv api error: {0}")] Api(String), + /// A bad moment while preparing media: a transient download status + /// (429 / 5xx), a stalled transfer or a temp-file write failure. A retry + /// can change the answer, so the pixiv retry policy re-fetches these. + #[error("transient pixiv error: {0}")] + Transient(String), } /// Native pixiv app-API client. @@ -228,6 +233,14 @@ impl PixivAPI { .await .map_err(|e| match e { FetchError::Http(e) => PixivError::Http(e), + // A bad moment (429/5xx, a stalled transfer, a temp-file + // write failure) must stay retryable: folding it into Api + // made one hiccup permanently fail the whole ugoira post, + // while the bot's own upload downloads retry the same + // classes. + transient @ (FetchError::Transient(_) | FetchError::Io(_)) => { + PixivError::Transient(format!("frame zip download failed: {transient}")) + } other => PixivError::Api(format!("frame zip download failed: {other}")), })?; let frame_delays = metadata.frames.iter().map(|f| f.delay).collect::>(); diff --git a/crates/x-media/src/site/pixiv/interface.rs b/crates/x-media/src/site/pixiv/interface.rs index 3c91bc0..dba8945 100644 --- a/crates/x-media/src/site/pixiv/interface.rs +++ b/crates/x-media/src/site/pixiv/interface.rs @@ -102,7 +102,7 @@ pub fn is_retryable(err: &FetchError) -> bool { /// rejected credential is not. fn pixiv_error_is_retryable(err: &PixivError) -> bool { match err { - PixivError::Http(_) => true, + PixivError::Http(_) | PixivError::Transient(_) => true, PixivError::Status(code) if *code == 429 || *code >= 500 => true, PixivError::Status(_) | PixivError::Api(_) | PixivError::Json(_) | PixivError::NoAuth => { false @@ -443,7 +443,11 @@ mod tests { // it calls `disable()`, a process-wide flag with no reset, so a test // touching it would order-couple every other pixiv test (the predicate // it keys on is covered by the table below). - for err in [PixivError::Status(429), PixivError::Status(503)] { + for err in [ + PixivError::Status(429), + PixivError::Status(503), + PixivError::Transient("frame zip download failed: transient".into()), + ] { let enabled_before = api::enabled(); let message = startup_validation(Err(err)).unwrap_err(); assert!(message.contains("stays enabled"), "{message}"); @@ -458,11 +462,15 @@ mod tests { #[test] fn is_retryable_classifies_transient_and_permanent() { - // Transient: network errors, explicit transient, pixiv 429/5xx. + // Transient: network errors, explicit transient, pixiv 429/5xx, and a + // failed media download (the frame zip's own bad moment). assert!(is_retryable(&FetchError::Transient("429".into()))); assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(429)))); assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(500)))); assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(503)))); + assert!(is_retryable(&FetchError::Pixiv(PixivError::Transient( + "frame zip download failed: transient: media status 429".into() + )))); // Permanent: pixiv 4xx (bad/expired token, forbidden, not found), // api/auth errors, unparseable bodies, not-found/blocked/sensitive. assert!(!is_retryable(&FetchError::Pixiv(PixivError::Status(400))));