Retry request from reverseproxy retry options

This commit is contained in:
euzu
2026-01-22 13:30:59 +01:00
parent 7dbd94792d
commit ccdbf1dfce
25 changed files with 414 additions and 369 deletions
+49 -94
View File
@@ -7,10 +7,10 @@ use crate::api::model::{create_channel_unavailable_stream, create_custom_video_s
use crate::api::model::{tee_stream, UserSession};
use crate::api::model::{ProviderAllocation, ProviderConfig, ProviderStreamState, StreamDetails, StreamingStrategy};
use crate::auth::Fingerprint;
use crate::model::{ConfigInput, ResourceRetryConfig};
use crate::model::{ConfigInput};
use crate::model::{ConfigTarget, ProxyUserCredentials};
use crate::tools::lru_cache::LRUResourceCache;
use crate::utils::request::{content_type_from_ext, parse_range};
use crate::utils::request::{content_type_from_ext, parse_range, send_with_retry};
use crate::utils::{async_file_reader, async_file_writer, create_new_file_for_write, get_file_extension};
use crate::utils::{debug_if_enabled, trace_if_enabled};
use crate::utils::request;
@@ -25,7 +25,6 @@ use chrono::{DateTime, Utc};
use futures::{stream, StreamExt, TryStreamExt};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use log::{debug, error, info, log_enabled, trace, warn};
use reqwest::header::RETRY_AFTER;
use serde::Serialize;
use shared::concat_string;
use shared::model::{Claims, InputFetchMethod, PlaylistEntry, PlaylistItemType, ProxyType, StreamChannel, TargetType, UserConnectionPermission, VirtualId, XtreamCluster};
@@ -39,7 +38,6 @@ use std::convert::Infallible;
use std::io::SeekFrom;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio::sync::Mutex;
use tokio_util::io::ReaderStream;
@@ -1343,102 +1341,59 @@ async fn fetch_resource_with_retry(
) -> Option<axum::response::Response> {
let config = app_state.app_config.config.load();
let default_user_agent = config.default_user_agent.clone();
let (max_attempts, backoff_ms, backoff_multiplier) = config
.reverse_proxy
.as_ref()
.map_or_else(ResourceRetryConfig::get_default_retry_values, |rp| rp.resource_retry.get_retry_values());
drop(config);
let disabled_headers = app_state.get_disabled_headers();
for attempt in 0..max_attempts {
let client = request::get_client_request(
&app_state.http_client.load(),
input.map_or(InputFetchMethod::GET, |i| i.method),
input.map(|i| &i.headers),
url,
Some(req_headers),
disabled_headers.as_ref(),
default_user_agent.as_deref(),
let response = match send_with_retry(
&app_state.app_config,
url,
|| {
request::get_client_request(
&app_state.http_client.load(),
input.map_or(InputFetchMethod::GET, |i| i.method),
input.map(|i| &i.headers),
url,
Some(req_headers),
disabled_headers.as_ref(),
default_user_agent.as_deref(),
)
},
)
.await
{
Ok(response) => response,
Err(_) => return None,
};
let status = response.status();
if status.is_success() {
return Some(
build_resource_stream_response(app_state, resource_url, response).await,
);
match client.send().await {
Ok(response) => {
let status = response.status();
if status.is_success() {
return Some(
build_resource_stream_response(app_state, resource_url, response).await,
);
}
// Retry only for 408, 425, 429 and all 5xx statuses
let should_retry = status.is_server_error()
|| matches!(
status,
// reqwest::StatusCode::BAD_REQUEST // 400 is typically client error; retrying likely won't help and adds load.
reqwest::StatusCode::REQUEST_TIMEOUT
| reqwest::StatusCode::TOO_EARLY
| reqwest::StatusCode::TOO_MANY_REQUESTS
);
if attempt < max_attempts - 1 && should_retry {
let wait_dur = response
.headers()
.get(RETRY_AFTER)
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.map_or_else(
|| {
let delay = calculate_retry_backoff(backoff_ms, backoff_multiplier, attempt);
Duration::from_millis(delay)
},
Duration::from_secs,
);
tokio::time::sleep(wait_dur).await;
continue;
}
// For non-retriable statuses or when attempts are exhausted, return upstream response including body
debug_if_enabled!(
"Failed to open resource got status {status} for {}",
sanitize_sensitive_info(resource_url)
);
let mut response_builder = axum::response::Response::builder().status(status);
for (key, value) in response.headers() {
response_builder = response_builder.header(key, value);
}
let stream = response
.bytes_stream()
.map_err(|err| StreamError::reqwest(&err));
return Some(try_unwrap_body!(
response_builder.body(axum::body::Body::from_stream(stream))
));
}
Err(err) => {
if attempt < max_attempts - 1 {
let delay = calculate_retry_backoff(backoff_ms, backoff_multiplier, attempt);
tokio::time::sleep(Duration::from_millis(delay)).await;
continue;
}
error!("Received failure from server {}: {err}", sanitize_sensitive_info(resource_url));
}
}
break;
}
None
// Non-retriable Status → Upstream Response incl. Body
debug_if_enabled!(
"Failed to open resource got status {status} for {}",
sanitize_sensitive_info(resource_url)
);
let mut response_builder = axum::response::Response::builder().status(status);
for (key, value) in response.headers() {
response_builder = response_builder.header(key, value);
}
let stream = response
.bytes_stream()
.map_err(|err| StreamError::reqwest(&err));
Some(try_unwrap_body!(
response_builder.body(axum::body::Body::from_stream(stream))
))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
fn calculate_retry_backoff(base_delay_ms: u64, multiplier: f64, attempt: u32) -> u64 {
let base = base_delay_ms.max(1);
if multiplier <= 1.0 {
return base;
}
let delay = (base as f64) * multiplier.powi(i32::try_from(attempt).unwrap_or(i32::MAX));
if !delay.is_finite() || delay < 1.0 {
base
} else if delay >= u64::MAX as f64 {
u64::MAX
} else {
delay as u64
}
}
/// # Panics
pub async fn resource_response(