Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/app-lib/src/util/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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")
Copy link
Contributor

@AlexTMjugador AlexTMjugador Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X-Ratelimit-Reset is a non-standard header, and the concept of rate-limiting headers beyond Retry-After has only recently seen some standardization efforts by the IETF. In particular, the latest draft proposal on the matter from last month, which is not yet a standard, states:

[...] different headers, with the same semantics, are used by different implementers:

  • X-RateLimit-Limit and X-Rate-Limit-Limit
  • X-RateLimit-Remaining and X-Rate-Limit-Remaining
  • X-RateLimit-Reset and X-Rate-Limit-Reset

With that in mind, I have two questions:

  • Which CDNs or file hosts you have seen sending this header? I tried hammering requests from several threads to one of the Modrinth CDN URLs listed in the .mrpack file attached to hitting a 429 rate limit cancels installing a .mrpack #3805, but I couldn't get any 429 responses. I also wasn't able to find any documentation from the CDN provider regarding this header.
  • Do the affected CDNs or file hosts also send the standard Retry-After header? If so, I'd prefer to use that one instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was based off of the Modrinth API ratelimit documentation which uses X-Ratelimit-Reset as number of seconds until the ratelimit resets. I will have to look into the portability of this, and seeing if Retry-After is a better solution

Copy link
Contributor

@AlexTMjugador AlexTMjugador Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see! That ratelimit documentation applies to API routes served by Labrinth, but the CDN endpoints from which project version files are downloaded don't go through Labrinth, so that documentation doesn't apply in this case.

Edit: nevertheless, it sounds plausible that during installation of a big .mrpack the app may be sending lots of separate requests to rate-limited endpoints, which may include some in our API or elsewhere. It'd be nice to track those down and confirm that's the case before merging this.

&& let Ok(seconds) = reset_header.to_str()
&& let Ok(seconds) = seconds.parse::<u64>()
&& attempt <= FETCH_ATTEMPTS
{
sleep(Duration::from_secs(seconds)).await;
continue;
}
if let Ok(error) = resp.json().await {
return Err(ErrorKind::LabrinthError(error).into());
}
Expand Down