fix(pixiv): keep a frame-zip download hiccup retryable

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.
This commit is contained in:
2026-09-24 02:50:22 +08:00
parent 38e65a3791
commit e6800fd27b
2 changed files with 24 additions and 3 deletions
+13
View File
@@ -39,6 +39,11 @@ pub enum PixivError {
Status(u16), Status(u16),
#[error("pixiv api error: {0}")] #[error("pixiv api error: {0}")]
Api(String), 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. /// Native pixiv app-API client.
@@ -228,6 +233,14 @@ impl PixivAPI {
.await .await
.map_err(|e| match e { .map_err(|e| match e {
FetchError::Http(e) => PixivError::Http(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}")), other => PixivError::Api(format!("frame zip download failed: {other}")),
})?; })?;
let frame_delays = metadata.frames.iter().map(|f| f.delay).collect::<Vec<_>>(); let frame_delays = metadata.frames.iter().map(|f| f.delay).collect::<Vec<_>>();
+11 -3
View File
@@ -102,7 +102,7 @@ pub fn is_retryable(err: &FetchError) -> bool {
/// rejected credential is not. /// rejected credential is not.
fn pixiv_error_is_retryable(err: &PixivError) -> bool { fn pixiv_error_is_retryable(err: &PixivError) -> bool {
match err { match err {
PixivError::Http(_) => true, PixivError::Http(_) | PixivError::Transient(_) => true,
PixivError::Status(code) if *code == 429 || *code >= 500 => true, PixivError::Status(code) if *code == 429 || *code >= 500 => true,
PixivError::Status(_) | PixivError::Api(_) | PixivError::Json(_) | PixivError::NoAuth => { PixivError::Status(_) | PixivError::Api(_) | PixivError::Json(_) | PixivError::NoAuth => {
false false
@@ -443,7 +443,11 @@ mod tests {
// it calls `disable()`, a process-wide flag with no reset, so a test // it calls `disable()`, a process-wide flag with no reset, so a test
// touching it would order-couple every other pixiv test (the predicate // touching it would order-couple every other pixiv test (the predicate
// it keys on is covered by the table below). // 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 enabled_before = api::enabled();
let message = startup_validation(Err(err)).unwrap_err(); let message = startup_validation(Err(err)).unwrap_err();
assert!(message.contains("stays enabled"), "{message}"); assert!(message.contains("stays enabled"), "{message}");
@@ -458,11 +462,15 @@ mod tests {
#[test] #[test]
fn is_retryable_classifies_transient_and_permanent() { 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::Transient("429".into())));
assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(429)))); assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(429))));
assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(500)))); assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(500))));
assert!(is_retryable(&FetchError::Pixiv(PixivError::Status(503)))); 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), // Permanent: pixiv 4xx (bad/expired token, forbidden, not found),
// api/auth errors, unparseable bodies, not-found/blocked/sensitive. // api/auth errors, unparseable bodies, not-found/blocked/sensitive.
assert!(!is_retryable(&FetchError::Pixiv(PixivError::Status(400)))); assert!(!is_retryable(&FetchError::Pixiv(PixivError::Status(400))));