From fce467e005dba678518335d219070ee8843d5981 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 4 Dec 2024 18:28:28 +0100 Subject: [PATCH] [feature] HTTP API timeouts (#290) * write an extractor for timeout * Use timeout * Default timeout 10min --- crates/librqbit/src/http_api.rs | 81 +++++++++++++++++++++++++-- crates/librqbit/webui/src/http-api.ts | 2 +- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index bd229be..69d3e59 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -82,6 +82,67 @@ async fn simple_basic_auth( } } +mod timeout { + use std::time::Duration; + + use anyhow::Context; + use axum::{extract::Query, RequestPartsExt}; + use http::request::Parts; + use serde::Deserialize; + + use crate::ApiError; + + pub struct Timeout(pub Duration); + + #[async_trait::async_trait] + impl axum::extract::FromRequestParts + for Timeout + where + S: Send + Sync, + { + type Rejection = ApiError; + + /// Perform the extraction. + async fn from_request_parts( + parts: &mut Parts, + _state: &S, + ) -> Result { + #[derive(Deserialize)] + struct QueryT { + timeout_ms: Option, + } + + let q = parts + .extract::>() + .await + .context("error running Timeout extractor")?; + + let timeout_ms = q + .timeout_ms + .map(Ok) + .or_else(|| { + parts + .headers + .get("x-req-timeout-ms") + .map(|v| { + std::str::from_utf8(v.as_bytes()) + .context("invalid utf-8 in timeout value") + }) + .map(|v| { + v.and_then(|v| v.parse::().context("invalid timeout integer")) + }) + }) + .transpose() + .context("error parsing timeout")? + .unwrap_or(DEFAULT_MS); + let timeout_ms = timeout_ms.min(MAX_MS); + Ok(Timeout(Duration::from_millis(timeout_ms as u64))) + } + } +} + +use timeout::Timeout; + impl HttpApi { pub fn new(api: Api, opts: Option) -> Self { Self { @@ -152,6 +213,7 @@ impl HttpApi { async fn torrents_post( State(state): State, Query(params): Query, + Timeout(timeout): Timeout<600_000, 3_600_000>, data: Bytes, ) -> Result { let is_url = params.is_url; @@ -185,7 +247,10 @@ impl HttpApi { } _ => AddTorrent::TorrentFileBytes(data.into()), }; - state.api_add_torrent(add, Some(opts)).await.map(axum::Json) + tokio::time::timeout(timeout, state.api_add_torrent(add, Some(opts))) + .await + .context("timeout")? + .map(axum::Json) } async fn torrent_details( @@ -257,19 +322,23 @@ impl HttpApi { async fn resolve_magnet( State(state): State, + Timeout(timeout): Timeout<600_000, 3_600_000>, inp_headers: HeaderMap, url: String, ) -> Result { - let added = state - .session() - .add_torrent( + let added = tokio::time::timeout( + timeout, + state.session().add_torrent( AddTorrent::from_url(&url), Some(AddTorrentOptions { list_only: true, ..Default::default() }), - ) - .await?; + ), + ) + .await + .context("timeout")??; + let (info, content) = match added { crate::AddTorrentResponse::AlreadyManaged(_, handle) => ( handle.shared().info.clone(), diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index 5db88c2..0c350d6 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -11,7 +11,7 @@ import { // Define API URL and base path const apiUrl = (() => { if (window.origin === "null" || window.origin === "http://localhost:3031") { - return "http://localhost:3030" + return "http://localhost:3030"; } let port = /http.*:\/\/.*:(\d+)/.exec(window.origin)?.[1]; if (port == "3031") {