diff --git a/src/admin/backfill.rs b/src/admin/backfill.rs index a1dfc9e..7974cd5 100644 --- a/src/admin/backfill.rs +++ b/src/admin/backfill.rs @@ -371,6 +371,7 @@ async fn discover_repos_from_relay( let r = state .http .get(&url) + .timeout(crate::http_retry::REQUEST_TIMEOUT) .send() .await .map_err(|e| format!("relay request failed: {e}"))?; @@ -1320,6 +1321,7 @@ async fn fetch_records_from_pds( let resp = state .http .get(&url) + .timeout(crate::http_retry::REQUEST_TIMEOUT) .send() .await .map_err(|e| format!("PDS request failed: {e}"))?; diff --git a/src/http_retry.rs b/src/http_retry.rs index c1edb28..c26255c 100644 --- a/src/http_retry.rs +++ b/src/http_retry.rs @@ -1,3 +1,12 @@ +use std::time::Duration; + +/// Per-request timeout for outbound atproto network fetches (relay, PLC/DID +/// resolution, PDS reads). Bounds a single HTTP attempt so a host that connects +/// but never responds fails instead of stalling a backfill job forever. This is +/// deliberately a per-request timeout, not a wall-clock deadline over a retry +/// loop, so rate-limit backoffs are left intact. +pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + /// Parse rate-limit sleep duration from response headers. /// Checks `RateLimit-Reset` (Unix timestamp, used by XRPC servers) first, /// then `retry-after` (seconds), defaulting to 5s. diff --git a/src/profile.rs b/src/profile.rs index 1a1d350..d9796aa 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::error::AppError; -use crate::http_retry::parse_retry_after; +use crate::http_retry::{REQUEST_TIMEOUT, parse_retry_after}; #[derive(Serialize)] pub struct Profile { @@ -147,6 +147,7 @@ pub async fn resolve_did_document( loop { let r = http .get(&url) + .timeout(REQUEST_TIMEOUT) .send() .await .map_err(|e| AppError::Internal(format!("DID resolution failed: {e}")))?;