From 5630a86d88b3873bedf744a9b75c1839973d0ee5 Mon Sep 17 00:00:00 2001 From: YoursFunny Date: Mon, 21 Sep 2026 17:09:07 +0800 Subject: [PATCH] refactor(x-media): share the HTTP-status error mapping twitter, twitter auth and bsky carried the same 404/410 -> NotFound, 401/403 -> Blocked, else Transient block (comments included). site::status_error holds it once. bilibili and misskey keep their own matches: neither maps 404/410 and each has a status the others do not (412 risk control, 400 + NO_SUCH_NOTE), so routing them through the shared block would have reclassified those statuses for the user. --- crates/x-media/src/site/bsky/interface.rs | 8 +------- crates/x-media/src/site/mod.rs | 15 +++++++++++++++ crates/x-media/src/site/twitter/auth.rs | 10 +--------- crates/x-media/src/site/twitter/interface.rs | 8 +------- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/crates/x-media/src/site/bsky/interface.rs b/crates/x-media/src/site/bsky/interface.rs index 00c3a8c..84afab2 100644 --- a/crates/x-media/src/site/bsky/interface.rs +++ b/crates/x-media/src/site/bsky/interface.rs @@ -321,13 +321,7 @@ pub async fn fetch(handle: &str, rkey: &str) -> Result { // 404/410 = gone (permanent); 429/5xx = transient and retried by fetch. let status = response.status(); if !status.is_success() { - return match status.as_u16() { - 404 | 410 => Err(FetchError::NotFound), - // A refusal or an auth demand is not a bad moment: retrying it - // three times only delays an error the user has to see. - 401 | 403 => Err(FetchError::Blocked), - _ => Err(FetchError::Transient(format!("bsky status {status}"))), - }; + return Err(crate::site::status_error("bsky", status)); } let text = response.text().await?; Post::from_json(&text, rkey.to_string()) diff --git a/crates/x-media/src/site/mod.rs b/crates/x-media/src/site/mod.rs index 76319f6..27e2fa5 100644 --- a/crates/x-media/src/site/mod.rs +++ b/crates/x-media/src/site/mod.rs @@ -281,6 +281,21 @@ pub enum FetchError { Io(std::io::Error), } +/// The error class for a non-success HTTP status, as the site adapters that +/// share this mapping classify it: 404/410 mean the post is gone and 401/403 a +/// refusal or an auth demand — both permanent, since retrying cannot change +/// either — while everything else (429, 5xx) is transient and retried by +/// [`fetch`]. `site` only names the adapter in the transient message; a site +/// whose statuses mean something else (bilibili's 412 risk control, misskey's +/// 400 with `NO_SUCH_NOTE`) maps those before falling back here. +pub fn status_error(site: &'static str, status: reqwest::StatusCode) -> FetchError { + match status.as_u16() { + 404 | 410 => FetchError::NotFound, + 401 | 403 => FetchError::Blocked, + _ => FetchError::Transient(format!("{site} status {status}")), + } +} + /// How long a download may make no progress: the response head, and then each /// individual chunk, must arrive within this window. Not a total timeout — see /// [`DOWNLOAD_TOTAL_TIMEOUT`]. diff --git a/crates/x-media/src/site/twitter/auth.rs b/crates/x-media/src/site/twitter/auth.rs index cfce069..c21e02e 100644 --- a/crates/x-media/src/site/twitter/auth.rs +++ b/crates/x-media/src/site/twitter/auth.rs @@ -130,15 +130,7 @@ pub async fn fetch(id: &str) -> Result { let status = response.status(); if !status.is_success() { log::warn!("twitter auth fetch {id}: HTTP {status}"); - return match status.as_u16() { - 404 | 410 => Err(FetchError::NotFound), - // A stale/refused `auth_token` is not a bad moment: retrying it - // three times only delays the report. - 401 | 403 => Err(FetchError::Blocked), - _ => Err(FetchError::Transient(format!( - "twitter auth status {status}" - ))), - }; + return Err(crate::site::status_error("twitter auth", status)); } let text = response.text().await?; let json: Value = serde_json::from_str(&text)?; diff --git a/crates/x-media/src/site/twitter/interface.rs b/crates/x-media/src/site/twitter/interface.rs index f54327a..5505e27 100644 --- a/crates/x-media/src/site/twitter/interface.rs +++ b/crates/x-media/src/site/twitter/interface.rs @@ -103,13 +103,7 @@ pub async fn fetch(id: &str) -> Result { // 404/410 = gone (permanent); 429/5xx = transient and retried by fetch. let status = response.status(); if !status.is_success() { - return match status.as_u16() { - 404 | 410 => Err(FetchError::NotFound), - // A refusal or an auth demand is not a bad moment: retrying it - // three times only delays an error the user has to see. - 401 | 403 => Err(FetchError::Blocked), - _ => Err(FetchError::Transient(format!("twitter status {status}"))), - }; + return Err(crate::site::status_error("twitter", status)); } let text = response.text().await?; // Classify before building the tweet (see [`parse_syndication_body`]), and