@@ -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+
85146impl 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 ( ) ,
0 commit comments