Skip to content

Commit fce467e

Browse files
authored
[feature] HTTP API timeouts (#290)
* write an extractor for timeout * Use timeout * Default timeout 10min
1 parent a1de63e commit fce467e

2 files changed

Lines changed: 76 additions & 7 deletions

File tree

crates/librqbit/src/http_api.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,67 @@ async fn simple_basic_auth(
8282
}
8383
}
8484

85+
mod timeout {
86+
use std::time::Duration;
87+
88+
use anyhow::Context;
89+
use axum::{extract::Query, RequestPartsExt};
90+
use http::request::Parts;
91+
use serde::Deserialize;
92+
93+
use crate::ApiError;
94+
95+
pub struct Timeout<const DEFAULT_MS: usize, const MAX_MS: usize>(pub Duration);
96+
97+
#[async_trait::async_trait]
98+
impl<S, const DEFAULT_MS: usize, const MAX_MS: usize> axum::extract::FromRequestParts<S>
99+
for Timeout<DEFAULT_MS, MAX_MS>
100+
where
101+
S: Send + Sync,
102+
{
103+
type Rejection = ApiError;
104+
105+
/// Perform the extraction.
106+
async fn from_request_parts(
107+
parts: &mut Parts,
108+
_state: &S,
109+
) -> Result<Self, Self::Rejection> {
110+
#[derive(Deserialize)]
111+
struct QueryT {
112+
timeout_ms: Option<usize>,
113+
}
114+
115+
let q = parts
116+
.extract::<Query<QueryT>>()
117+
.await
118+
.context("error running Timeout extractor")?;
119+
120+
let timeout_ms = q
121+
.timeout_ms
122+
.map(Ok)
123+
.or_else(|| {
124+
parts
125+
.headers
126+
.get("x-req-timeout-ms")
127+
.map(|v| {
128+
std::str::from_utf8(v.as_bytes())
129+
.context("invalid utf-8 in timeout value")
130+
})
131+
.map(|v| {
132+
v.and_then(|v| v.parse::<usize>().context("invalid timeout integer"))
133+
})
134+
})
135+
.transpose()
136+
.context("error parsing timeout")?
137+
.unwrap_or(DEFAULT_MS);
138+
let timeout_ms = timeout_ms.min(MAX_MS);
139+
Ok(Timeout(Duration::from_millis(timeout_ms as u64)))
140+
}
141+
}
142+
}
143+
144+
use timeout::Timeout;
145+
85146
impl HttpApi {
86147
pub fn new(api: Api, opts: Option<HttpApiOptions>) -> Self {
87148
Self {
@@ -152,6 +213,7 @@ impl HttpApi {
152213
async fn torrents_post(
153214
State(state): State<ApiState>,
154215
Query(params): Query<TorrentAddQueryParams>,
216+
Timeout(timeout): Timeout<600_000, 3_600_000>,
155217
data: Bytes,
156218
) -> Result<impl IntoResponse> {
157219
let is_url = params.is_url;
@@ -185,7 +247,10 @@ impl HttpApi {
185247
}
186248
_ => AddTorrent::TorrentFileBytes(data.into()),
187249
};
188-
state.api_add_torrent(add, Some(opts)).await.map(axum::Json)
250+
tokio::time::timeout(timeout, state.api_add_torrent(add, Some(opts)))
251+
.await
252+
.context("timeout")?
253+
.map(axum::Json)
189254
}
190255

191256
async fn torrent_details(
@@ -257,19 +322,23 @@ impl HttpApi {
257322

258323
async fn resolve_magnet(
259324
State(state): State<ApiState>,
325+
Timeout(timeout): Timeout<600_000, 3_600_000>,
260326
inp_headers: HeaderMap,
261327
url: String,
262328
) -> Result<impl IntoResponse> {
263-
let added = state
264-
.session()
265-
.add_torrent(
329+
let added = tokio::time::timeout(
330+
timeout,
331+
state.session().add_torrent(
266332
AddTorrent::from_url(&url),
267333
Some(AddTorrentOptions {
268334
list_only: true,
269335
..Default::default()
270336
}),
271-
)
272-
.await?;
337+
),
338+
)
339+
.await
340+
.context("timeout")??;
341+
273342
let (info, content) = match added {
274343
crate::AddTorrentResponse::AlreadyManaged(_, handle) => (
275344
handle.shared().info.clone(),

crates/librqbit/webui/src/http-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
// Define API URL and base path
1212
const apiUrl = (() => {
1313
if (window.origin === "null" || window.origin === "http://localhost:3031") {
14-
return "http://localhost:3030"
14+
return "http://localhost:3030";
1515
}
1616
let port = /http.*:\/\/.*:(\d+)/.exec(window.origin)?.[1];
1717
if (port == "3031") {

0 commit comments

Comments
 (0)