From b690bf2761ae00efd01cf5fc98d2086151a7d256 Mon Sep 17 00:00:00 2001 From: Jocelyn Castellano Date: Fri, 31 Oct 2025 23:56:29 -0500 Subject: [PATCH 1/3] Retry 429s after waiting a specified time --- packages/app-lib/src/util/fetch.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index fafba22152..e4d5f209b0 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -10,9 +10,9 @@ use serde::de::DeserializeOwned; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::LazyLock; -use std::time::{self}; +use std::time::{self, Duration}; use tokio::sync::Semaphore; -use tokio::{fs::File, io::AsyncWriteExt}; +use tokio::{fs::File, io::AsyncWriteExt, time::sleep}; #[derive(Debug)] pub struct IoSemaphore(pub Semaphore); @@ -116,6 +116,16 @@ pub async fn fetch_advanced( || resp.status().is_server_error() { let backup_error = resp.error_for_status_ref().unwrap_err(); + if resp.status() == 429 + && let Some(reset_header) = + resp.headers().get("X-Ratelimit-Reset") + && let Ok(seconds) = reset_header.to_str() + && let Ok(seconds) = seconds.parse::() + && attempt <= FETCH_ATTEMPTS + { + sleep(Duration::from_secs(seconds)).await; + continue; + } if let Ok(error) = resp.json().await { return Err(ErrorKind::LabrinthError(error).into()); } From 1d4c07ad70a7bc86a15caa62dad1cabb0b1ec96c Mon Sep 17 00:00:00 2001 From: Jocelyn Castellano Date: Thu, 20 Nov 2025 01:07:27 -0600 Subject: [PATCH 2/3] Improve portability of ratelimit detection --- packages/app-lib/src/util/fetch.rs | 51 +++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index e4d5f209b0..a4b6e3d827 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -5,6 +5,7 @@ use crate::LAUNCHER_USER_AGENT; use crate::event::LoadingBarId; use crate::event::emit::emit_loading; use bytes::Bytes; +use chrono::DateTime; use reqwest::Method; use serde::de::DeserializeOwned; use std::ffi::OsStr; @@ -77,13 +78,14 @@ pub async fn fetch_advanced( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { let _permit = semaphore.0.acquire().await?; + let is_modrinth = url.starts_with("https://cdn.modrinth.com") + || url.starts_with(env!("MODRINTH_API_URL")) + || url.starts_with(env!("MODRINTH_API_URL_V3")); let creds = if header .as_ref() .is_none_or(|x| &*x.0.to_lowercase() != "authorization") - && (url.starts_with("https://cdn.modrinth.com") - || url.starts_with(env!("MODRINTH_API_URL")) - || url.starts_with(env!("MODRINTH_API_URL_V3"))) + && is_modrinth { crate::state::ModrinthCredentials::get_active(exec).await? } else { @@ -116,15 +118,39 @@ pub async fn fetch_advanced( || resp.status().is_server_error() { let backup_error = resp.error_for_status_ref().unwrap_err(); - if resp.status() == 429 - && let Some(reset_header) = - resp.headers().get("X-Ratelimit-Reset") - && let Ok(seconds) = reset_header.to_str() - && let Ok(seconds) = seconds.parse::() - && attempt <= FETCH_ATTEMPTS - { - sleep(Duration::from_secs(seconds)).await; - continue; + if resp.status() == 429 && attempt <= FETCH_ATTEMPTS { + if is_modrinth // x-ratelimit-reset is not portable across different servers + && let Some(reset_header) = + resp.headers().get("X-Ratelimit-Reset") + && let Ok(seconds) = reset_header.to_str() + && let Ok(seconds) = seconds.parse::() + { + sleep(Duration::from_secs(seconds)).await; + continue; + } else if let Some(retry_header) = + resp.headers().get("Retry-After") + && let Ok(retry) = retry_header.to_str() + { + if let Ok(seconds) = retry.parse::() { + // when retry-after retruns a delay in seconds + sleep(Duration::from_secs(seconds)).await; + continue; + } else if let Ok(date) = + DateTime::parse_from_rfc2822(retry) + // when retry-after returns an http date + { + let now = chrono::Utc::now(); + // Convert now to the same timezone as date + let now_fixed = + now.with_timezone(date.offset()); + let wait_duration = date - now_fixed; + sleep(Duration::from_secs( + wait_duration.num_seconds() as u64, + )) + .await; + continue; + } + } } if let Ok(error) = resp.json().await { return Err(ErrorKind::LabrinthError(error).into()); @@ -163,6 +189,7 @@ pub async fn fetch_advanced( if let Some(sha1) = sha1 { let hash = sha1_async(bytes.clone()).await?; if &*hash != sha1 { + dbg!(&bytes); if attempt <= FETCH_ATTEMPTS { continue; } else { From 0e2f3e014f012873ededb658336edba714076553 Mon Sep 17 00:00:00 2001 From: pandaninjas Date: Fri, 21 Nov 2025 02:55:11 -0600 Subject: [PATCH 3/3] Remove extra dbg macro --- packages/app-lib/src/util/fetch.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-lib/src/util/fetch.rs b/packages/app-lib/src/util/fetch.rs index a4b6e3d827..678294327c 100644 --- a/packages/app-lib/src/util/fetch.rs +++ b/packages/app-lib/src/util/fetch.rs @@ -189,7 +189,6 @@ pub async fn fetch_advanced( if let Some(sha1) = sha1 { let hash = sha1_async(bytes.clone()).await?; if &*hash != sha1 { - dbg!(&bytes); if attempt <= FETCH_ATTEMPTS { continue; } else {