mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
site: classify HTTP status codes, make transient failures retryable
Twitter (syndication + auth GraphQL) mapped every non-2xx to NotFound, killing retries on 429/5xx; bsky never checked status; pixiv network errors arrived wrapped in PixivError and were excluded from the retry loop. New FetchError::Transient covers 429/5xx from all sites, the retry loop now also retries Pixiv errors, and 404/410 stay permanent.
This commit is contained in:
@@ -189,6 +189,14 @@ pub async fn fetch(handle: &str, rkey: &str) -> Result<Post, FetchError> {
|
|||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
// 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),
|
||||||
|
_ => Err(FetchError::Transient(format!("bsky status {status}"))),
|
||||||
|
};
|
||||||
|
}
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
Ok(Post::from_json(&text, rkey.to_string())?)
|
Ok(Post::from_json(&text, rkey.to_string())?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ pub enum FetchError {
|
|||||||
Sensitive,
|
Sensitive,
|
||||||
/// A download exceeded the caller's size cap (see [`download_media_limited`]).
|
/// A download exceeded the caller's size cap (see [`download_media_limited`]).
|
||||||
TooLarge,
|
TooLarge,
|
||||||
|
/// A transient server-side failure (429 / 5xx); [`fetch`] retries these.
|
||||||
|
Transient(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FetchError {
|
impl fmt::Display for FetchError {
|
||||||
@@ -162,6 +164,7 @@ impl fmt::Display for FetchError {
|
|||||||
FetchError::Blocked => write!(f, "blocked"),
|
FetchError::Blocked => write!(f, "blocked"),
|
||||||
FetchError::Sensitive => write!(f, "content withheld (sensitive)"),
|
FetchError::Sensitive => write!(f, "content withheld (sensitive)"),
|
||||||
FetchError::TooLarge => write!(f, "media too large"),
|
FetchError::TooLarge => write!(f, "media too large"),
|
||||||
|
FetchError::Transient(message) => write!(f, "transient: {message}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +177,7 @@ impl std::error::Error for FetchError {
|
|||||||
FetchError::Pixiv(e) => Some(e),
|
FetchError::Pixiv(e) => Some(e),
|
||||||
FetchError::NotFound | FetchError::Blocked | FetchError::Sensitive => None,
|
FetchError::NotFound | FetchError::Blocked | FetchError::Sensitive => None,
|
||||||
FetchError::TooLarge => None,
|
FetchError::TooLarge => None,
|
||||||
|
FetchError::Transient(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,9 +246,10 @@ pub(crate) fn log_once_ffmpeg_missing() {
|
|||||||
/// matches (unsupported links are silently ignored by the bot).
|
/// matches (unsupported links are silently ignored by the bot).
|
||||||
///
|
///
|
||||||
/// Transient network failures are retried: 3 total attempts with 1s then 2s
|
/// Transient network failures are retried: 3 total attempts with 1s then 2s
|
||||||
/// delays. Non-Http errors (Json/NotFound/Blocked/Pixiv) are not retried.
|
/// delays. Retried classes: bare HTTP errors, [`FetchError::Transient`]
|
||||||
|
/// (429/5xx from any site), and pixiv errors (its network failures arrive
|
||||||
|
/// wrapped as `PixivError`). Non-retried: Json/NotFound/Blocked/Sensitive.
|
||||||
pub async fn fetch(url: &str) -> Result<Option<Fetched>, FetchError> {
|
pub async fn fetch(url: &str) -> Result<Option<Fetched>, FetchError> {
|
||||||
let mut last_http_error = None;
|
|
||||||
for attempt in 0..3u32 {
|
for attempt in 0..3u32 {
|
||||||
match fetch_once(url).await {
|
match fetch_once(url).await {
|
||||||
Ok(Some(fetched)) => {
|
Ok(Some(fetched)) => {
|
||||||
@@ -256,18 +261,17 @@ pub async fn fetch(url: &str) -> Result<Option<Fetched>, FetchError> {
|
|||||||
return Ok(Some(fetched));
|
return Ok(Some(fetched));
|
||||||
}
|
}
|
||||||
Ok(None) => return Ok(None),
|
Ok(None) => return Ok(None),
|
||||||
Err(FetchError::Http(e)) => {
|
Err(e @ (FetchError::Http(_) | FetchError::Transient(_) | FetchError::Pixiv(_))) => {
|
||||||
last_http_error = Some(e);
|
|
||||||
if attempt < 2 {
|
if attempt < 2 {
|
||||||
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
|
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(other) => return Err(other),
|
Err(other) => return Err(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(FetchError::Http(
|
unreachable!("retry loop always returns")
|
||||||
last_http_error.expect("retry loop always ran 3 attempts"),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_once(url: &str) -> Result<Option<Fetched>, FetchError> {
|
async fn fetch_once(url: &str) -> Result<Option<Fetched>, FetchError> {
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ impl PixivAPI {
|
|||||||
.bearer_auth(access_token)
|
.bearer_auth(access_token)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(PixivError::Api(format!(
|
||||||
|
"status {}",
|
||||||
|
response.status()
|
||||||
|
)));
|
||||||
|
}
|
||||||
let json: serde_json::Value = serde_json::from_str(&response.text().await?)?;
|
let json: serde_json::Value = serde_json::from_str(&response.text().await?)?;
|
||||||
if json.get("error").is_some() {
|
if json.get("error").is_some() {
|
||||||
let message = json
|
let message = json
|
||||||
@@ -186,6 +192,12 @@ impl PixivAPI {
|
|||||||
.bearer_auth(access_token)
|
.bearer_auth(access_token)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(PixivError::Api(format!(
|
||||||
|
"status {}",
|
||||||
|
response.status()
|
||||||
|
)));
|
||||||
|
}
|
||||||
let json: serde_json::Value = serde_json::from_str(&response.text().await?)?;
|
let json: serde_json::Value = serde_json::from_str(&response.text().await?)?;
|
||||||
if json.get("error").is_some() {
|
if json.get("error").is_some() {
|
||||||
let message = json
|
let message = json
|
||||||
|
|||||||
@@ -126,9 +126,14 @@ pub async fn fetch(id: &str) -> Result<Tweet, FetchError> {
|
|||||||
.header("referer", "https://x.com/")
|
.header("referer", "https://x.com/")
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if !response.status().is_success() {
|
// 404/410 = gone (permanent); 429/5xx = transient and retried by fetch.
|
||||||
log::warn!("twitter auth fetch {id}: HTTP {}", response.status());
|
let status = response.status();
|
||||||
return Err(FetchError::NotFound);
|
if !status.is_success() {
|
||||||
|
log::warn!("twitter auth fetch {id}: HTTP {status}");
|
||||||
|
return match status.as_u16() {
|
||||||
|
404 | 410 => Err(FetchError::NotFound),
|
||||||
|
_ => Err(FetchError::Transient(format!("twitter auth status {status}"))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
let json: Value = serde_json::from_str(&text)?;
|
let json: Value = serde_json::from_str(&text)?;
|
||||||
|
|||||||
@@ -68,8 +68,13 @@ pub async fn fetch(id: &str) -> Result<Tweet, FetchError> {
|
|||||||
))
|
))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if !response.status().is_success() {
|
// 404/410 = gone (permanent); 429/5xx = transient and retried by fetch.
|
||||||
return Err(FetchError::NotFound);
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
return match status.as_u16() {
|
||||||
|
404 | 410 => Err(FetchError::NotFound),
|
||||||
|
_ => Err(FetchError::Transient(format!("twitter status {status}"))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
// Deleted tweets answer with {"errors": [...]} instead of a tweet.
|
// Deleted tweets answer with {"errors": [...]} instead of a tweet.
|
||||||
|
|||||||
Reference in New Issue
Block a user