@@ -9,7 +9,6 @@ use hyper::body::{Body as _, Bytes, Frame, Incoming};
99use hyper_rustls:: { HttpsConnector , HttpsConnectorBuilder } ;
1010use hyper_util:: client:: legacy:: Client ;
1111use hyper_util:: client:: legacy:: connect:: HttpConnector ;
12- use hyper_util:: rt:: TokioExecutor ;
1312use tower_service:: Service ;
1413
1514use super :: config:: HttpConfig ;
@@ -22,10 +21,37 @@ const HTTP_MAX_HEADERS: usize = 100;
2221
2322type BoxConnectError = Box < dyn Error + Send + Sync > ;
2423
24+ tokio:: task_local! {
25+ /// Immutable per-hop budget, including policy preflight. Never keyed by host.
26+ static CONNECT_DEADLINE : Instant ;
27+ }
28+
29+ /// Hyper can move a started connection race into its executor when pool checkout
30+ /// wins (hyper-util 0.1.20, legacy/client.rs). Capture at submission, not polling:
31+ /// connector readiness/call may then run on another task. Dispatcher tasks carry
32+ /// context only; response headers and bodies are not timed by this deadline.
33+ #[ derive( Clone , Copy ) ]
34+ struct HttpExecutor ;
35+
36+ impl < F > hyper:: rt:: Executor < F > for HttpExecutor
37+ where
38+ F : Future + Send + ' static ,
39+ F :: Output : Send + ' static ,
40+ {
41+ fn execute ( & self , future : F ) {
42+ let deadline = CONNECT_DEADLINE . try_with ( |deadline| * deadline) . ok ( ) ;
43+ tokio:: spawn ( async move {
44+ match deadline {
45+ Some ( deadline) => CONNECT_DEADLINE . scope ( deadline, future) . await ,
46+ None => future. await ,
47+ }
48+ } ) ;
49+ }
50+ }
51+
2552#[ derive( Clone ) ]
2653pub ( super ) struct ConnectTimeout < C > {
2754 inner : C ,
28- timeout : std:: time:: Duration ,
2955}
3056
3157impl < C > Service < hyper:: Uri > for ConnectTimeout < C >
@@ -44,10 +70,19 @@ where
4470 }
4571
4672 fn call ( & mut self , destination : hyper:: Uri ) -> Self :: Future {
73+ let Ok ( deadline) = CONNECT_DEADLINE . try_with ( |deadline| * deadline) else {
74+ // Missing propagation must fail closed, never restart a full budget.
75+ return Box :: pin ( async {
76+ Err ( BoxConnectError :: from ( std:: io:: Error :: other (
77+ "HTTP connect phase deadline context missing" ,
78+ ) ) )
79+ } ) ;
80+ } ;
4781 let future = self . inner . call ( destination) ;
48- let timeout = self . timeout ;
82+ #[ cfg( test) ]
83+ let future = deadline_tests:: delay_connect ( future) ;
4984 Box :: pin ( async move {
50- tokio:: time:: timeout ( timeout , future)
85+ tokio:: time:: timeout_at ( tokio :: time :: Instant :: from_std ( deadline ) , future)
5186 . await
5287 . map_err ( |_| {
5388 BoxConnectError :: from ( std:: io:: Error :: new (
@@ -75,11 +110,8 @@ pub(super) fn build_client(config: &HttpConfig) -> HttpClient {
75110 . https_or_http ( )
76111 . enable_http1 ( )
77112 . wrap_connector ( http) ;
78- let connector = ConnectTimeout {
79- inner : connector,
80- timeout : config. connect_timeout ,
81- } ;
82- let mut builder = Client :: builder ( TokioExecutor :: new ( ) ) ;
113+ let connector = ConnectTimeout { inner : connector } ;
114+ let mut builder = Client :: builder ( HttpExecutor ) ;
83115 builder
84116 . http1_max_buf_size ( HTTP_MAX_HEAD_BYTES )
85117 . http1_max_headers ( HTTP_MAX_HEADERS ) ;
@@ -91,7 +123,12 @@ async fn validate_target_until(
91123 url : & url:: Url ,
92124 deadline : Instant ,
93125) -> VmResult < ( ) > {
94- let resolved = with_deadline ( deadline, resolve_url ( config, SchemeFamily :: Http , url) ) . await ?;
126+ let resolved = with_deadline ( deadline, async {
127+ #[ cfg( test) ]
128+ deadline_tests:: delay_preflight ( ) . await ;
129+ resolve_url ( config, SchemeFamily :: Http , url) . await
130+ } )
131+ . await ?;
95132 debug_assert_eq ! ( resolved. host, url. host_str( ) . unwrap_or_default( ) ) ;
96133 debug_assert_eq ! (
97134 resolved. address. port( ) ,
@@ -312,7 +349,15 @@ async fn execute_request_until(
312349 // policy resolver repeats the private-address check for the address
313350 // used by any newly opened pooled connection.
314351 validate_target_until ( config, & url, connect_deadline) . await ?;
315- let mut response = send_request ( client, & method, & url, & headers, body. as_deref ( ) ) . await ?;
352+ let mut response = send_request (
353+ client,
354+ & method,
355+ & url,
356+ & headers,
357+ body. as_deref ( ) ,
358+ connect_deadline,
359+ )
360+ . await ?;
316361 validate_response_framing ( response. response ( ) ) ?;
317362 if follows_location ( response. response ( ) . status ( ) ) {
318363 if redirect_index == config. max_redirects {
@@ -428,7 +473,15 @@ pub(super) async fn open_stream_response(
428473 for redirect_index in 0 ..=config. max_redirects {
429474 let connect_deadline = phase_deadline ( opening_deadline, config. connect_timeout ) ;
430475 validate_target_until ( config, & url, connect_deadline) . await ?;
431- let response = send_request ( client, & method, & url, & headers, body. as_deref ( ) ) . await ?;
476+ let response = send_request (
477+ client,
478+ & method,
479+ & url,
480+ & headers,
481+ body. as_deref ( ) ,
482+ connect_deadline,
483+ )
484+ . await ?;
432485 * bytes_sent = bytes_sent. saturating_add ( body. as_ref ( ) . map_or ( 0 , Vec :: len) ) ;
433486 validate_response_framing ( response. response ( ) ) ?;
434487 if follows_location ( response. response ( ) . status ( ) ) {
@@ -473,6 +526,7 @@ async fn send_request(
473526 url : & url:: Url ,
474527 headers : & [ ( hyper:: header:: HeaderName , hyper:: header:: HeaderValue ) ] ,
475528 body : Option < & [ u8 ] > ,
529+ connect_deadline : Instant ,
476530) -> VmResult < OwnedResponse > {
477531 let uri = url
478532 . as_str ( )
@@ -485,16 +539,19 @@ async fn send_request(
485539 let request = builder
486540 . body ( Full :: new ( Bytes :: copy_from_slice ( body. unwrap_or_default ( ) ) ) )
487541 . map_err ( |error| VmError :: HostError ( format ! ( "HTTP request setup failed: {error}" ) ) ) ?;
488- let response = client. request ( request) . await . map_err ( |error| {
489- let mut message = format ! ( "HTTP request failed: {error}" ) ;
490- let mut source = error. source ( ) ;
491- while let Some ( error) = source {
492- message. push_str ( ": " ) ;
493- message. push_str ( & error. to_string ( ) ) ;
494- source = error. source ( ) ;
495- }
496- VmError :: HostError ( message)
497- } ) ?;
542+ let response = CONNECT_DEADLINE
543+ . scope ( connect_deadline, client. request ( request) )
544+ . await
545+ . map_err ( |error| {
546+ let mut message = format ! ( "HTTP request failed: {error}" ) ;
547+ let mut source = error. source ( ) ;
548+ while let Some ( error) = source {
549+ message. push_str ( ": " ) ;
550+ message. push_str ( & error. to_string ( ) ) ;
551+ source = error. source ( ) ;
552+ }
553+ VmError :: HostError ( message)
554+ } ) ?;
498555 Ok ( OwnedResponse { response } )
499556}
500557
@@ -712,6 +769,9 @@ fn reject_declared_oversize(
712769 Ok ( ( ) )
713770}
714771
772+ #[ cfg( test) ]
773+ mod deadline_tests;
774+
715775#[ cfg( test) ]
716776mod tests {
717777 use super :: { RequestHeaderBudget , validate_resource_request, validate_response_trailers} ;
0 commit comments