• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

0xmichalis / nftbk / 18346462606

08 Oct 2025 01:33PM UTC coverage: 54.854% (-0.9%) from 55.73%
18346462606

Pull #45

github

web-flow
Merge 9146fc44e into c1d3177fb
Pull Request #45: HTTP client with configurable IPFS gateways into its own module

399 of 536 new or added lines in 11 files covered. (74.44%)

1 existing line in 1 file now uncovered.

3554 of 6479 relevant lines covered (54.85%)

2.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.44
/src/httpclient/fetch.rs
1
use tracing::{info, warn};
2

3
use crate::url::{all_ipfs_gateway_urls_with_gateways, IpfsGatewayConfig};
4

5
pub(crate) async fn fetch_url(url: &str) -> anyhow::Result<reqwest::Response> {
8✔
6
    let client = reqwest::Client::builder()
8✔
7
        .user_agent(crate::USER_AGENT)
8✔
8
        .build()?;
8✔
9

10
    Ok(client.get(url).send().await?)
8✔
11
}
8✔
12

13
pub(crate) fn create_http_error(status: reqwest::StatusCode, url: &str) -> anyhow::Error {
6✔
14
    anyhow::anyhow!("HTTP error: status {status} from {url}")
6✔
15
}
6✔
16

17
pub(crate) async fn try_fetch_response(
8✔
18
    url: &str,
8✔
19
    gateways: &[IpfsGatewayConfig],
8✔
20
) -> (
8✔
21
    anyhow::Result<reqwest::Response>,
8✔
22
    Option<reqwest::StatusCode>,
8✔
23
) {
8✔
24
    match fetch_url(url).await {
8✔
25
        Ok(response) => {
8✔
26
            let status = response.status();
8✔
27
            if status.is_success() {
8✔
28
                (Ok(response), Some(status))
2✔
29
            } else {
30
                let error = create_http_error(status, url);
6✔
31
                match retry_with_gateways(url, error, gateways).await {
6✔
NEW
32
                    Ok(alt_response) => {
×
NEW
33
                        let alt_status = alt_response.status();
×
NEW
34
                        if alt_status.is_success() {
×
NEW
35
                            (Ok(alt_response), Some(alt_status))
×
36
                        } else {
NEW
37
                            (
×
NEW
38
                                Err(create_http_error(alt_status, alt_response.url().as_str())),
×
NEW
39
                                Some(alt_status),
×
NEW
40
                            )
×
41
                        }
42
                    }
43
                    Err(gateway_err) => (Err(gateway_err), Some(status)),
6✔
44
                }
45
            }
46
        }
NEW
47
        Err(e) => match retry_with_gateways(url, e, gateways).await {
×
NEW
48
            Ok(response) => {
×
NEW
49
                let status = response.status();
×
NEW
50
                if status.is_success() {
×
NEW
51
                    (Ok(response), Some(status))
×
52
                } else {
NEW
53
                    (Err(create_http_error(status, url)), Some(status))
×
54
                }
55
            }
NEW
56
            Err(gateway_err) => (Err(gateway_err), None),
×
57
        },
58
    }
59
}
8✔
60

61
pub(crate) async fn retry_with_gateways(
6✔
62
    url: &str,
6✔
63
    original_error: anyhow::Error,
6✔
64
    gateways: &[IpfsGatewayConfig],
6✔
65
) -> anyhow::Result<reqwest::Response> {
6✔
66
    warn!(
6✔
NEW
67
        "IPFS gateway error for {}, retrying with other gateways: {}",
×
68
        url, original_error
69
    );
70

71
    let gateway_urls = match all_ipfs_gateway_urls_with_gateways(url, gateways) {
6✔
72
        Some(urls) => urls,
4✔
73
        None => return Err(original_error),
2✔
74
    };
75

76
    let mut last_err = original_error;
4✔
77
    let alternative_gateways: Vec<_> = gateway_urls
4✔
78
        .into_iter()
4✔
79
        .filter(|gateway_url| gateway_url != url)
4✔
80
        .collect();
4✔
81

82
    for new_url in alternative_gateways {
4✔
NEW
83
        match fetch_url(&new_url).await {
×
NEW
84
            Ok(response) => {
×
NEW
85
                let status = response.status();
×
NEW
86
                if status.is_success() {
×
NEW
87
                    info!(
×
NEW
88
                        "Received successful response from alternative IPFS gateway: {} (status: {})",
×
89
                        new_url, status
90
                    );
NEW
91
                    return Ok(response);
×
92
                } else {
NEW
93
                    warn!("Alternative IPFS gateway {new_url} failed: {status}");
×
NEW
94
                    last_err = anyhow::anyhow!("{status}");
×
95
                }
96
            }
NEW
97
            Err(err) => {
×
NEW
98
                warn!("Failed to fetch from IPFS gateway {}: {}", new_url, err);
×
NEW
99
                last_err = err;
×
100
            }
101
        }
102
    }
103

104
    Err(last_err)
4✔
105
}
6✔
106

107
#[cfg(test)]
108
mod tests {
109
    use wiremock::{
110
        matchers::{method, path},
111
        Mock, MockServer, ResponseTemplate,
112
    };
113

114
    #[tokio::test]
115
    async fn test_http_200_success() {
1✔
116
        let mock_server = MockServer::start().await;
1✔
117
        let url = format!("{}/success", mock_server.uri());
1✔
118

119
        Mock::given(method("GET"))
1✔
120
            .and(path("/success"))
1✔
121
            .respond_with(ResponseTemplate::new(200).set_body_string("Hello, World!"))
1✔
122
            .mount(&mock_server)
1✔
123
            .await;
1✔
124

125
        let client = crate::httpclient::HttpClient::new();
1✔
126
        let (res, _status) = client.try_fetch_response(&url).await;
1✔
127
        assert!(res.is_ok());
1✔
128
        let body = res.unwrap().bytes().await.unwrap();
1✔
129
        assert_eq!(body.as_ref(), b"Hello, World!");
1✔
130
    }
1✔
131

132
    #[tokio::test]
133
    async fn test_http_404_no_retry() {
1✔
134
        let mock_server = MockServer::start().await;
1✔
135
        let url = format!("{}/not-found", mock_server.uri());
1✔
136

137
        Mock::given(method("GET"))
1✔
138
            .and(path("/not-found"))
1✔
139
            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1✔
140
            .expect(1)
1✔
141
            .mount(&mock_server)
1✔
142
            .await;
1✔
143

144
        let client = crate::httpclient::HttpClient::new();
1✔
145
        let (res, _status) = client.try_fetch_response(&url).await;
1✔
146
        assert!(res.is_err());
1✔
147
        let err = res.err().unwrap().to_string();
1✔
148
        assert!(err.contains("HTTP error: status 404"));
1✔
149
    }
1✔
150

151
    #[tokio::test]
152
    async fn test_http_500_retry_and_fail() {
1✔
153
        let mock_server = MockServer::start().await;
1✔
154
        let url = format!("{}/server-error", mock_server.uri());
1✔
155

156
        Mock::given(method("GET"))
1✔
157
            .and(path("/server-error"))
1✔
158
            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1✔
159
            .expect(1)
1✔
160
            .mount(&mock_server)
1✔
161
            .await;
1✔
162

163
        let client = crate::httpclient::HttpClient::new();
1✔
164
        let (res, _status) = client.try_fetch_response(&url).await;
1✔
165
        assert!(res.is_err());
1✔
166
        let err = res.err().unwrap().to_string();
1✔
167
        assert!(err.contains("HTTP error: status 500"));
1✔
168
    }
1✔
169
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc