From e6800fd27b1e65e95b862bb263b02ebdd5080204 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Thu, 24 Sep 2026 02:50:22 +0800 Subject: [PATCH] fix(pixiv): keep a frame-zip download hiccup retryable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit download_media_to_file's non-transport errors all folded into PixivError::Api, and Api is permanent in pixiv_error_is_retryable — so one 429/5xx, stalled transfer or temp-file write failure while fetching the ugoira frame zip permanently failed the whole post, while the bot's own upload downloads classify the very same classes as retryable (classify_download_error). A new PixivError::Transient carries those classes into the existing retry policy (and into startup validation's 'stays enabled' branch); Http keeps its arm and everything else stays permanent. The retryable table and the startup-validation loop pin both halves. --- crates/x-media/src/site/pixiv/api.rs | 13 +++++++++++++ crates/x-media/src/site/pixiv/interface.rs | 14 +++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) 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))));