fire_scope/
fetch.rs

1use crate::constants::MAX_RIR_DOWNLOAD_BYTES;
2use crate::error::AppError;
3use futures::StreamExt;
4use rand::Rng;
5use reqwest::Client;
6use std::time::Duration;
7use tokio::time::sleep;
8use crate::common::debug_log;
9
10/// ボディをストリーミングで読み込みつつ、サイズ上限を強制してStringへ変換
11async fn read_body_with_limit_to_string(
12    resp: reqwest::Response,
13    max_bytes: u64,
14) -> Result<String, AppError> {
15    let mut total: u64 = 0;
16    let mut buf: Vec<u8> = Vec::new();
17
18    let mut stream = resp.bytes_stream();
19    while let Some(chunk) = stream.next().await {
20        let chunk = chunk?; // reqwest::Error -> AppError::Network via ? 上位で変換
21        total = total.saturating_add(chunk.len() as u64);
22        if total > max_bytes {
23            return Err(AppError::Other(format!(
24                "Response too large ({} bytes > {} bytes)",
25                total, max_bytes
26            )));
27        }
28        buf.extend_from_slice(&chunk);
29    }
30
31    let text = String::from_utf8(buf)?; // FromUtf8Error -> AppError::Utf8
32    Ok(text)
33}
34
35async fn fetch_once(client: &Client, url: &str) -> Result<String, AppError> {
36    let resp = client.get(url).send().await?.error_for_status()?; // 非2xxを明示的にエラー化
37
38    if let Some(len) = resp.content_length() {
39        if len > MAX_RIR_DOWNLOAD_BYTES {
40            return Err(AppError::Other(format!(
41                "Response too large ({} bytes > {} bytes): {}",
42                len, MAX_RIR_DOWNLOAD_BYTES, url
43            )));
44        }
45    }
46    // Content-Length が無い場合にも備えて、常にストリーミングで上限制御
47    read_body_with_limit_to_string(resp, MAX_RIR_DOWNLOAD_BYTES).await
48}
49
50/// HTTP GETによるデータ取得をリトライ+指数バックオフ付きで行う
51/// 失敗時はAppError::Other(...)を返す
52pub async fn fetch_with_retry(
53    client: &Client,
54    url: &str,
55    retry_attempts: u32,
56    max_backoff_secs: u64,
57) -> Result<String, AppError> {
58    let attempts = retry_attempts.max(1);
59    for i in 0..attempts {
60        match fetch_once(client, url).await {
61            Ok(text) => {
62                return Ok(text);
63            }
64            Err(e) => {
65                debug_log(format!(
66                    "fetch attempt {}/{} failed: {}",
67                    i + 1,
68                    attempts,
69                    e
70                ));
71                // 最終試行後はスリープせずに即エラー復帰
72                if i + 1 < attempts {
73                    let sleep_duration = calc_exponential_backoff_duration(i, max_backoff_secs);
74                    sleep(sleep_duration).await;
75                }
76            }
77        }
78    }
79
80    // リトライ失敗
81    Err(AppError::Other(format!(
82        "Failed to fetch data from {} after {} attempts",
83        url, attempts
84    )))
85}
86
87/// 指数バックオフのスリープ時間を計算するヘルパー関数
88fn calc_exponential_backoff_duration(retry_count: u32, max_backoff_secs: u64) -> Duration {
89    // Full Jitter
90    // wait ~ Uniform(0, min(cap, 2^retry))
91    let mut rng = rand::rng();
92    let exp = 2u64.saturating_pow(retry_count);
93    let cap = max_backoff_secs.max(1);
94    let range = exp.min(cap) as f64;
95    let wait_secs = rng.random::<f64>() * range;
96    Duration::from_secs_f64(wait_secs)
97}
98
99/// JSONをサイズ上限制御の上で取得してパース
100pub async fn fetch_json_with_limit<T: serde::de::DeserializeOwned>(
101    client: &Client,
102    url: &str,
103    max_bytes: u64,
104) -> Result<T, AppError> {
105    let resp = client.get(url).send().await?.error_for_status()?;
106
107    if let Some(len) = resp.content_length() {
108        if len > max_bytes {
109            return Err(AppError::Other(format!(
110                "JSON response too large ({} bytes > {} bytes): {}",
111                len, max_bytes, url
112            )));
113        }
114    }
115
116    // ボディを上限制御で読み込む
117    let text = read_body_with_limit_to_string(resp, max_bytes).await?;
118    let value = serde_json::from_str::<T>(&text)
119        .map_err(|e| AppError::ParseError(format!("JSON parse error: {e}")))?;
120    Ok(value)
121}