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

0xmichalis / nftbk / 18786425165

24 Oct 2025 04:50PM UTC coverage: 52.991% (+7.8%) from 45.155%
18786425165

push

github

0xmichalis
refactor: group functionality better in run_server

0 of 43 new or added lines in 1 file covered. (0.0%)

246 existing lines in 21 files now uncovered.

2994 of 5650 relevant lines covered (52.99%)

9.31 hits per line

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

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

3
use crate::ipfs::config::IpfsGatewayConfig;
4
use crate::ipfs::url::{generate_url_for_gateways, GatewayUrl};
5

6
pub(crate) async fn fetch_url(
18✔
7
    url: &str,
8
    bearer_token: Option<String>,
9
) -> anyhow::Result<reqwest::Response> {
10
    let client = reqwest::Client::builder()
36✔
11
        .user_agent(crate::USER_AGENT)
18✔
12
        .build()?;
13

14
    let mut req = client.get(url);
72✔
15
    if let Some(token) = bearer_token {
18✔
UNCOV
16
        req = req.bearer_auth(token);
×
17
    }
18

19
    Ok(req.send().await?)
38✔
20
}
21

22
pub(crate) fn create_http_error(status: reqwest::StatusCode, url: &str) -> anyhow::Error {
8✔
23
    anyhow::anyhow!("HTTP error: status {status} from {url}")
16✔
24
}
25

26
pub(crate) async fn try_fetch_response(
10✔
27
    url: &str,
28
    gateways: &[IpfsGatewayConfig],
29
) -> (
30
    anyhow::Result<reqwest::Response>,
31
    Option<reqwest::StatusCode>,
32
) {
33
    match fetch_url(url, None).await {
40✔
34
        Ok(response) => {
9✔
35
            let status = response.status();
27✔
36
            if status.is_success() {
18✔
37
                (Ok(response), Some(status))
1✔
38
            } else {
39
                let error = create_http_error(status, url);
32✔
40
                match retry_with_gateways(url, error, gateways).await {
40✔
41
                    Ok(alt_response) => {
×
UNCOV
42
                        let alt_status = alt_response.status();
×
UNCOV
43
                        if alt_status.is_success() {
×
44
                            (Ok(alt_response), Some(alt_status))
×
45
                        } else {
46
                            (
47
                                Err(create_http_error(alt_status, alt_response.url().as_str())),
×
48
                                Some(alt_status),
×
49
                            )
50
                        }
51
                    }
52
                    Err(gateway_err) => (Err(gateway_err), Some(status)),
16✔
53
                }
54
            }
55
        }
56
        Err(e) => match retry_with_gateways(url, e, gateways).await {
6✔
57
            Ok(response) => {
1✔
58
                let status = response.status();
3✔
59
                if status.is_success() {
2✔
60
                    (Ok(response), Some(status))
1✔
61
                } else {
62
                    (Err(create_http_error(status, url)), Some(status))
×
63
                }
64
            }
65
            Err(gateway_err) => (Err(gateway_err), None),
×
66
        },
67
    }
68
}
69

70
pub(crate) async fn retry_with_gateways(
14✔
71
    url: &str,
72
    original_error: anyhow::Error,
73
    gateways: &[IpfsGatewayConfig],
74
) -> anyhow::Result<reqwest::Response> {
75
    let gateway_urls = match generate_url_for_gateways(url, gateways) {
33✔
76
        Some(urls) => {
5✔
77
            warn!(
5✔
78
                "IPFS gateway error for {}, retrying with other gateways: {}",
×
79
                url, original_error
80
            );
81
            urls
5✔
82
        }
83
        None => return Err(original_error),
9✔
84
    };
85

86
    let mut last_err = original_error;
10✔
87
    let alternative_gateways: Vec<_> = gateway_urls
15✔
88
        .into_iter()
89
        .filter(|gateway_url| gateway_url.url != url)
33✔
90
        .collect();
91

92
    for GatewayUrl {
93
        url: new_url,
8✔
94
        bearer_token,
8✔
95
    } in alternative_gateways
9✔
96
    {
97
        match fetch_url(&new_url, bearer_token).await {
32✔
98
            Ok(response) => {
7✔
99
                let status = response.status();
21✔
100
                if status.is_success() {
14✔
101
                    info!(
4✔
102
                        "Received successful response from alternative IPFS gateway: {} (status: {})",
×
103
                        new_url, status
104
                    );
105
                    return Ok(response);
4✔
106
                } else {
107
                    warn!("Alternative IPFS gateway {new_url} failed: {status}");
3✔
108
                    last_err = anyhow::anyhow!("{status}");
9✔
109
                }
110
            }
111
            Err(err) => {
1✔
112
                warn!("Failed to fetch from IPFS gateway {}: {}", new_url, err);
1✔
113
                last_err = err;
2✔
114
            }
115
        }
116
    }
117

118
    Err(last_err)
1✔
119
}
120

121
#[cfg(test)]
122
mod try_fetch_response_tests {
123
    use wiremock::{
124
        matchers::{method, path},
125
        Mock, MockServer, ResponseTemplate,
126
    };
127

128
    use crate::ipfs::config::{IpfsGatewayConfig, IpfsGatewayType};
129

130
    fn leak_str(s: String) -> &'static str {
131
        Box::leak(s.into_boxed_str())
132
    }
133

134
    fn gw(base: &str) -> IpfsGatewayConfig {
135
        IpfsGatewayConfig {
136
            url: leak_str(base.to_string()),
137
            gateway_type: IpfsGatewayType::Path,
138
            bearer_token_env: None,
139
        }
140
    }
141

142
    #[tokio::test]
143
    async fn test_http_200_success() {
144
        let mock_server = MockServer::start().await;
145
        let url = format!("{}/success", mock_server.uri());
146

147
        Mock::given(method("GET"))
148
            .and(path("/success"))
149
            .respond_with(ResponseTemplate::new(200).set_body_string("Hello, World!"))
150
            .mount(&mock_server)
151
            .await;
152

153
        let (res, status) = super::try_fetch_response(&url, &[]).await;
154
        assert!(status.is_some());
155
        assert_eq!(status.unwrap(), reqwest::StatusCode::OK);
156
        assert!(res.is_ok());
157
        let body = res.unwrap().bytes().await.unwrap();
158
        assert_eq!(body.as_ref(), b"Hello, World!");
159
    }
160

161
    #[tokio::test]
162
    async fn test_http_404_no_retry() {
163
        let mock_server = MockServer::start().await;
164
        let url = format!("{}/not-found", mock_server.uri());
165

166
        Mock::given(method("GET"))
167
            .and(path("/not-found"))
168
            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
169
            .expect(1)
170
            .mount(&mock_server)
171
            .await;
172

173
        let (res, status) = super::try_fetch_response(&url, &[]).await;
174
        assert!(status.is_some());
175
        assert_eq!(status.unwrap(), reqwest::StatusCode::NOT_FOUND);
176
        assert!(res.is_err());
177
        let err = res.err().unwrap().to_string();
178
        assert!(err.contains("HTTP error: status 404"));
179
    }
180

181
    #[tokio::test]
182
    async fn test_http_500_retry_and_fail() {
183
        let mock_server = MockServer::start().await;
184
        let url = format!("{}/server-error", mock_server.uri());
185

186
        Mock::given(method("GET"))
187
            .and(path("/server-error"))
188
            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
189
            .expect(1)
190
            .mount(&mock_server)
191
            .await;
192

193
        let (res, status) = super::try_fetch_response(&url, &[]).await;
194
        assert!(status.is_some());
195
        assert_eq!(status.unwrap(), reqwest::StatusCode::INTERNAL_SERVER_ERROR);
196
        assert!(res.is_err());
197
        let err = res.err().unwrap().to_string();
198
        assert!(err.contains("HTTP error: status 500"));
199
    }
200

201
    #[tokio::test]
202
    async fn test_initial_error_then_alternative_gateway_succeeds() {
203
        // Arrange: original gateway is a dead socket, alternative returns 200
204
        let server_b = MockServer::start().await;
205
        let dead_gateway_base = "http://127.0.0.1:9"; // simulate connect error
206

207
        let content_path = "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/ok";
208
        let original_url = format!("{}/ipfs/{}", dead_gateway_base, content_path);
209

210
        // B returns 200 for the same IPFS path
211
        Mock::given(method("GET"))
212
            .and(path(format!("/ipfs/{}", content_path)))
213
            .respond_with(ResponseTemplate::new(200).set_body_string("ALT OK"))
214
            .mount(&server_b)
215
            .await;
216

217
        // Gateways: first the dead one (original), then the working mock
218
        let gateways = vec![gw(dead_gateway_base), gw(&server_b.uri())];
219

220
        // Act
221
        let (res, status) = super::try_fetch_response(&original_url, &gateways).await;
222

223
        // Assert
224
        assert!(res.is_ok());
225
        assert_eq!(status, Some(reqwest::StatusCode::OK));
226
        let body = res.unwrap().bytes().await.unwrap();
227
        assert_eq!(body.as_ref(), b"ALT OK");
228
    }
229
}
230

231
#[cfg(test)]
232
mod retry_with_gateways_tests {
233
    use wiremock::{
234
        matchers::{method, path},
235
        Mock, MockServer, ResponseTemplate,
236
    };
237

238
    use crate::ipfs::config::{IpfsGatewayConfig, IpfsGatewayType};
239

240
    // Helper to leak a String into a 'static str for IpfsGatewayConfig
241
    fn leak_str(s: String) -> &'static str {
242
        Box::leak(s.into_boxed_str())
243
    }
244

245
    // Build a path-based gateway config from a base URL
246
    fn gw(base: &str) -> IpfsGatewayConfig {
247
        IpfsGatewayConfig {
248
            url: leak_str(base.to_string()),
249
            gateway_type: IpfsGatewayType::Path,
250
            bearer_token_env: None,
251
        }
252
    }
253

254
    #[tokio::test]
255
    async fn first_alternative_succeeds() {
256
        // Arrange gateways A(original), B(success), C(not used)
257
        let server_a = MockServer::start().await;
258
        let server_b = MockServer::start().await;
259
        let server_c = MockServer::start().await;
260

261
        let content_path = "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/ok";
262
        let original_url = format!("{}/ipfs/{}", server_a.uri(), content_path);
263

264
        // B returns 200
265
        Mock::given(method("GET"))
266
            .and(path(format!("/ipfs/{}", content_path)))
267
            .respond_with(ResponseTemplate::new(200).set_body_string("B OK"))
268
            .mount(&server_b)
269
            .await;
270

271
        let gateways = vec![
272
            gw(&server_a.uri()),
273
            gw(&server_b.uri()),
274
            gw(&server_c.uri()),
275
        ];
276

277
        // Act
278
        let res =
279
            super::retry_with_gateways(&original_url, anyhow::anyhow!("orig err"), &gateways).await;
280

281
        // Assert
282
        assert!(res.is_ok());
283
        let body = res.unwrap().bytes().await.unwrap();
284
        assert_eq!(body.as_ref(), b"B OK");
285
    }
286

287
    #[tokio::test]
288
    async fn first_alternative_http_error_second_succeeds() {
289
        // Arrange gateways A(original), B(500), C(200)
290
        let server_a = MockServer::start().await;
291
        let server_b = MockServer::start().await;
292
        let server_c = MockServer::start().await;
293

294
        let content_path = "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/ok2";
295
        let original_url = format!("{}/ipfs/{}", server_a.uri(), content_path);
296

297
        // B returns 500
298
        Mock::given(method("GET"))
299
            .and(path(format!("/ipfs/{}", content_path)))
300
            .respond_with(ResponseTemplate::new(500))
301
            .mount(&server_b)
302
            .await;
303

304
        // C returns 200
305
        Mock::given(method("GET"))
306
            .and(path(format!("/ipfs/{}", content_path)))
307
            .respond_with(ResponseTemplate::new(200).set_body_string("C OK"))
308
            .mount(&server_c)
309
            .await;
310

311
        let gateways = vec![
312
            gw(&server_a.uri()),
313
            gw(&server_b.uri()),
314
            gw(&server_c.uri()),
315
        ];
316

317
        // Act
318
        let res =
319
            super::retry_with_gateways(&original_url, anyhow::anyhow!("orig err"), &gateways).await;
320

321
        // Assert
322
        assert!(res.is_ok());
323
        let body = res.unwrap().bytes().await.unwrap();
324
        assert_eq!(body.as_ref(), b"C OK");
325
    }
326

327
    #[tokio::test]
328
    async fn first_alternative_non_http_error_second_succeeds() {
329
        // Arrange gateways A(original), B(non-http error), C(200)
330
        let server_a = MockServer::start().await;
331
        let server_c = MockServer::start().await;
332

333
        // Use an unroutable/closed port for B to simulate network error
334
        let dead_gateway_base = "http://127.0.0.1:9"; // port 9 (discard) likely closed
335

336
        let content_path = "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/ok3";
337
        let original_url = format!("{}/ipfs/{}", server_a.uri(), content_path);
338

339
        // C returns 200
340
        Mock::given(method("GET"))
341
            .and(path(format!("/ipfs/{}", content_path)))
342
            .respond_with(ResponseTemplate::new(200).set_body_string("C OK"))
343
            .mount(&server_c)
344
            .await;
345

346
        let gateways = vec![
347
            gw(&server_a.uri()),
348
            gw(dead_gateway_base),
349
            gw(&server_c.uri()),
350
        ];
351

352
        // Act
353
        let res =
354
            super::retry_with_gateways(&original_url, anyhow::anyhow!("orig err"), &gateways).await;
355

356
        // Assert
357
        assert!(res.is_ok());
358
        let body = res.unwrap().bytes().await.unwrap();
359
        assert_eq!(body.as_ref(), b"C OK");
360
    }
361

362
    #[tokio::test]
363
    async fn both_alternatives_fail() {
364
        // Arrange gateways A(original), B(500), C(502)
365
        let server_a = MockServer::start().await;
366
        let server_b = MockServer::start().await;
367
        let server_c = MockServer::start().await;
368

369
        let content_path = "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/fail";
370
        let original_url = format!("{}/ipfs/{}", server_a.uri(), content_path);
371

372
        // B returns 500
373
        Mock::given(method("GET"))
374
            .and(path(format!("/ipfs/{}", content_path)))
375
            .respond_with(ResponseTemplate::new(500))
376
            .mount(&server_b)
377
            .await;
378

379
        // C returns 502
380
        Mock::given(method("GET"))
381
            .and(path(format!("/ipfs/{}", content_path)))
382
            .respond_with(ResponseTemplate::new(502))
383
            .mount(&server_c)
384
            .await;
385

386
        let gateways = vec![
387
            gw(&server_a.uri()),
388
            gw(&server_b.uri()),
389
            gw(&server_c.uri()),
390
        ];
391

392
        // Act
393
        let res =
394
            super::retry_with_gateways(&original_url, anyhow::anyhow!("orig err"), &gateways).await;
395

396
        // Assert: error should reflect the last error (502)
397
        assert!(res.is_err());
398
        let err = res.err().unwrap().to_string();
399
        assert!(err.contains("502"));
400
    }
401

402
    #[tokio::test]
403
    async fn non_ipfs_url_returns_original_error_without_warning() {
404
        // Test that non-IPFS URLs return the original error without logging IPFS gateway warnings
405
        let non_ipfs_url =
406
            "https://mirror.xyz/10/0xceda033195af537e16d203a4d7fbfe1c5f0eb843/render";
407
        let original_error_msg = "HTTP error: status 429 Too Many Requests";
408
        let original_error = anyhow::anyhow!(original_error_msg);
409
        let gateways = vec![gw("https://ipfs.io")];
410

411
        // Act
412
        let res = super::retry_with_gateways(non_ipfs_url, original_error, &gateways).await;
413

414
        // Assert: should return the original error without trying IPFS gateways
415
        assert!(res.is_err());
416
        let err = res.err().unwrap();
417
        assert_eq!(err.to_string(), original_error_msg);
418
    }
419
}
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