Skip to content

Commit fff0175

Browse files
committed
Add HTTP status checking and improve error messages when fetching mirrors
1 parent 49f8a17 commit fff0175

File tree

16 files changed

+93
-180
lines changed

16 files changed

+93
-180
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.25.0 (2026-01-16)
2+
3+
- improved error messages when fetching mirrors fails:
4+
- HTTP errors (429, 500, etc.) now show status code and URL instead of "error decoding response body"
5+
- timeout errors now include the URL instead of showing empty string
6+
- JSON/text decoding errors now include the URL for context
7+
18
# 0.24.0 (2026-01-16)
29

310
- added base option `--exclude-countries` to skip mirrors from certain

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rate_mirrors"
3-
version = "0.24.0"
3+
version = "0.25.0"
44
authors = ["Nikita Almakov <nikita.almakov@gmail.com>"]
55
edition = "2024"
66
description = "Everyday-use client-side map-aware mirror ranking tool (Arch Linux; Manjaro; custom ones)"

src/config.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ use crate::target_configs::stdin::StdinTarget;
1717
use ambassador::{delegatable_trait, Delegate};
1818
use clap::{Parser, Subcommand};
1919
use itertools::Itertools;
20+
use serde::de::DeserializeOwned;
2021
use std::collections::HashSet;
2122
use std::fmt;
2223
use std::str::FromStr;
2324
use std::sync::{mpsc, Arc};
25+
use std::time::Duration;
2426
use thiserror::Error;
27+
use tokio::runtime::Runtime;
2528
use url::Url;
2629

2730
#[derive(Debug, PartialEq, Clone)]
@@ -49,6 +52,8 @@ pub enum AppError {
4952
RequestTimeout(String),
5053
#[error("{0}")]
5154
RequestError(String),
55+
#[error("HTTP {status} from {url}")]
56+
HttpError { status: u16, url: String },
5257
#[error("no mirrors after filtering")]
5358
NoMirrorsAfterFiltering,
5459
#[error(transparent)]
@@ -330,3 +335,57 @@ impl Config {
330335
.next()
331336
}
332337
}
338+
339+
fn convert_reqwest_error(e: reqwest::Error, url: &str) -> AppError {
340+
if e.is_timeout() {
341+
AppError::RequestTimeout(url.to_string())
342+
} else {
343+
AppError::RequestError(format!("failed to connect to {}: {}", url, e))
344+
}
345+
}
346+
347+
pub fn fetch_json<T: DeserializeOwned>(url: &str, timeout_ms: u64) -> Result<T, AppError> {
348+
Runtime::new().unwrap().block_on(async {
349+
let response = reqwest::Client::new()
350+
.get(url)
351+
.timeout(Duration::from_millis(timeout_ms))
352+
.send()
353+
.await
354+
.map_err(|e| convert_reqwest_error(e, url))?;
355+
356+
let status = response.status();
357+
if !status.is_success() {
358+
return Err(AppError::HttpError {
359+
status: status.as_u16(),
360+
url: url.to_string(),
361+
});
362+
}
363+
364+
response.json::<T>().await.map_err(|e| {
365+
AppError::RequestError(format!("failed to decode JSON from {}: {}", url, e))
366+
})
367+
})
368+
}
369+
370+
pub fn fetch_text(url: &str, timeout_ms: u64) -> Result<String, AppError> {
371+
Runtime::new().unwrap().block_on(async {
372+
let response = reqwest::Client::new()
373+
.get(url)
374+
.timeout(Duration::from_millis(timeout_ms))
375+
.send()
376+
.await
377+
.map_err(|e| convert_reqwest_error(e, url))?;
378+
379+
let status = response.status();
380+
if !status.is_success() {
381+
return Err(AppError::HttpError {
382+
status: status.as_u16(),
383+
url: url.to_string(),
384+
});
385+
}
386+
387+
response.text_with_charset("utf-8").await.map_err(|e| {
388+
AppError::RequestError(format!("failed to read response from {}: {}", url, e))
389+
})
390+
})
391+
}

src/targets/archarm.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_text, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::mirror::Mirror;
33
use crate::target_configs::archarm::ArcharmTarget;
4-
use reqwest;
54
use std::fmt::Display;
65
use std::sync::{mpsc, Arc};
7-
use std::time::Duration;
8-
use tokio::runtime::Runtime;
96
use url::Url;
107

118
impl LogFormatter for ArcharmTarget {
@@ -32,17 +29,7 @@ impl FetchMirrors for ArcharmTarget {
3229
) -> Result<Vec<Mirror>, AppError> {
3330
let url = "https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman-mirrorlist/mirrorlist";
3431

35-
let output = Runtime::new().unwrap().block_on(async {
36-
Ok::<_, AppError>(
37-
reqwest::Client::new()
38-
.get(url)
39-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
40-
.send()
41-
.await?
42-
.text_with_charset("utf-8")
43-
.await?,
44-
)
45-
})?;
32+
let output = fetch_text(url, self.fetch_mirrors_timeout)?;
4633

4734
let urls = output
4835
.lines()

src/targets/archlinux.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_json, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::countries::Country;
33
use crate::mirror::Mirror;
44
use crate::target_configs::archlinux::{ArchMirrorsSortingStrategy, ArchTarget};
55
use rand::prelude::SliceRandom;
66
use rand::rng;
7-
use reqwest;
87
use serde::Deserialize;
98
use std::fmt::Display;
109
use std::sync::{mpsc, Arc};
11-
use std::time::Duration;
12-
use tokio::runtime::Runtime;
1310
use url::Url;
1411

1512
#[derive(Deserialize, Debug, Clone)]
@@ -50,17 +47,7 @@ impl FetchMirrors for ArchTarget {
5047
"https://archlinux.org/mirrors/status/json/"
5148
};
5249

53-
let mirrors_data = Runtime::new().unwrap().block_on(async {
54-
Ok::<_, AppError>(
55-
reqwest::Client::new()
56-
.get(url)
57-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
58-
.send()
59-
.await?
60-
.json::<ArchMirrorsData>()
61-
.await?,
62-
)
63-
})?;
50+
let mirrors_data: ArchMirrorsData = fetch_json(url, self.fetch_mirrors_timeout)?;
6451

6552
tx_progress
6653
.send(format!("FETCHED MIRRORS: {}", mirrors_data.urls.len()))

src/targets/archlinuxcn.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_text, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::mirror::Mirror;
33
use crate::target_configs::archlinuxcn::ArchCNTarget;
4-
use reqwest;
54
use std::fmt::Display;
65
use std::sync::{mpsc, Arc};
7-
use std::time::Duration;
8-
use tokio::runtime::Runtime;
96
use url::Url;
107

118
impl LogFormatter for ArchCNTarget {
@@ -32,17 +29,7 @@ impl FetchMirrors for ArchCNTarget {
3229
) -> Result<Vec<Mirror>, AppError> {
3330
let url = "https://raw.githubusercontent.com/archlinuxcn/mirrorlist-repo/master/archlinuxcn-mirrorlist";
3431

35-
let output = Runtime::new().unwrap().block_on(async {
36-
Ok::<_, AppError>(
37-
reqwest::Client::new()
38-
.get(url)
39-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
40-
.send()
41-
.await?
42-
.text_with_charset("utf-8")
43-
.await?,
44-
)
45-
})?;
32+
let output = fetch_text(url, self.fetch_mirrors_timeout)?;
4633

4734
let urls = output
4835
.lines()

src/targets/arcolinux.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_text, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::mirror::Mirror;
33
use crate::target_configs::arcolinux::ArcoLinuxTarget;
4-
use reqwest;
54
use std::fmt::Display;
65
use std::sync::{mpsc, Arc};
7-
use std::time::Duration;
8-
use tokio::runtime::Runtime;
96
use url::Url;
107

118
impl LogFormatter for ArcoLinuxTarget {
@@ -27,17 +24,7 @@ impl FetchMirrors for ArcoLinuxTarget {
2724
let url =
2825
"https://raw.githubusercontent.com/arcolinux/arcolinux-mirrorlist/refs/heads/master/etc/pacman.d/arcolinux-mirrorlist";
2926

30-
let output = Runtime::new().unwrap().block_on(async {
31-
Ok::<_, AppError>(
32-
reqwest::Client::new()
33-
.get(url)
34-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
35-
.send()
36-
.await?
37-
.text_with_charset("utf-8")
38-
.await?,
39-
)
40-
})?;
27+
let output = fetch_text(url, self.fetch_mirrors_timeout)?;
4128

4229
let urls = output
4330
.lines()

src/targets/artix.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_text, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::countries::Country;
33
use crate::mirror::Mirror;
44
use crate::target_configs::artix::ArtixTarget;
5-
use reqwest;
65
use std::fmt::Display;
76
use std::sync::{Arc, mpsc};
8-
use std::time::Duration;
9-
use tokio::runtime::Runtime;
107
use url::Url;
118

129
impl LogFormatter for ArtixTarget {
@@ -27,17 +24,7 @@ impl FetchMirrors for ArtixTarget {
2724
) -> Result<Vec<Mirror>, AppError> {
2825
let url = "https://packages.artixlinux.org/mirrorlist/all/";
2926

30-
let output = Runtime::new().unwrap().block_on(async {
31-
Ok::<_, AppError>(
32-
reqwest::Client::new()
33-
.get(url)
34-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
35-
.send()
36-
.await?
37-
.text_with_charset("utf-8")
38-
.await?,
39-
)
40-
})?;
27+
let output = fetch_text(url, self.fetch_mirrors_timeout)?;
4128

4229
let mut current_country = None;
4330
let mut mirrors = Vec::new();

src/targets/blackarch.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
use crate::config::{AppError, Config, FetchMirrors, LogFormatter};
1+
use crate::config::{fetch_text, AppError, Config, FetchMirrors, LogFormatter};
22
use crate::countries::Country;
33
use crate::mirror::Mirror;
44
use crate::target_configs::blackarch::BlackArchTarget;
5-
use reqwest;
65
use std::fmt::Display;
76
use std::sync::{mpsc, Arc};
8-
use std::time::Duration;
9-
use tokio::runtime::Runtime;
107
use url::Url;
118

129
impl LogFormatter for BlackArchTarget {
@@ -37,17 +34,7 @@ impl FetchMirrors for BlackArchTarget {
3734
//
3835
// http://mirror.surf/blackarch/blackarch/os/x86_64/blackarch.files
3936

40-
let output = Runtime::new().unwrap().block_on(async {
41-
Ok::<_, AppError>(
42-
reqwest::Client::new()
43-
.get(url)
44-
.timeout(Duration::from_millis(self.fetch_mirrors_timeout))
45-
.send()
46-
.await?
47-
.text_with_charset("utf-8")
48-
.await?,
49-
)
50-
})?;
37+
let output = fetch_text(url, self.fetch_mirrors_timeout)?;
5138

5239
let mirrors: Vec<Mirror> = output
5340
.lines()

0 commit comments

Comments
 (0)