@@ -8,14 +8,11 @@ use bitreq::RequestExt;
88
99use std:: convert:: TryFrom ;
1010use std:: fmt;
11- use std:: net:: { SocketAddr , ToSocketAddrs } ;
1211use std:: time:: Duration ;
1312
14- /// Timeout for reading the first byte of a response. This is separate from the general read
15- /// timeout as it is not uncommon for Bitcoin Core to be blocked waiting on UTXO cache flushes for
16- /// upwards of 10 minutes on slow devices (e.g. RPis with SSDs over USB). Note that we always retry
17- /// once when we time out, so the maximum time we allow Bitcoin Core to block for is twice this
18- /// value.
13+ /// Timeout for requests. This is set to a high value as it is not uncommon for Bitcoin Core to be
14+ /// blocked waiting on UTXO cache flushes for upwards of 10 minutes on slow devices (e.g. RPis with
15+ /// SSDs over USB).
1916const TCP_STREAM_RESPONSE_TIMEOUT : Duration = Duration :: from_secs ( 300 ) ;
2017
2118/// Maximum HTTP message body size in bytes. Enough for a hex-encoded block in JSON format and any
@@ -48,7 +45,7 @@ impl fmt::Display for HttpClientError {
4845 match self {
4946 HttpClientError :: Transport ( e) => write ! ( f, "transport error: {}" , e) ,
5047 HttpClientError :: Http ( e) => write ! ( f, "HTTP error: {}" , e) ,
51- HttpClientError :: Io ( e) => write ! ( f, "I/O error: {}" , e) ,
48+ HttpClientError :: Io ( e) => write ! ( f, "Response parsing/conversion error: {}" , e) ,
5249 }
5350 }
5451}
@@ -130,49 +127,38 @@ const MAX_CONNECTIONS: usize = 10;
130127
131128/// Client for making HTTP requests.
132129pub ( crate ) struct HttpClient {
133- address : SocketAddr ,
130+ host : String ,
131+ port : u16 ,
134132 #[ cfg( feature = "tokio" ) ]
135133 client : bitreq:: Client ,
136134}
137135
138136impl HttpClient {
139- /// Opens a connection to an HTTP endpoint.
140- pub fn connect < E : ToSocketAddrs > ( endpoint : E ) -> Result < Self , HttpClientError > {
141- let address = match endpoint. to_socket_addrs ( ) ?. next ( ) {
142- None => {
143- return Err ( std:: io:: Error :: new (
144- std:: io:: ErrorKind :: InvalidInput ,
145- "could not resolve to any addresses" ,
146- )
147- . into ( ) ) ;
148- } ,
149- Some ( address) => address,
150- } ;
151-
152- Ok ( Self {
153- address,
137+ /// Creates a new HTTP client for the given endpoint.
138+ ///
139+ /// DNS resolution is deferred until the first request is made.
140+ pub fn new ( endpoint : & HttpEndpoint ) -> Self {
141+ Self {
142+ host : endpoint. host ( ) . to_string ( ) ,
143+ port : endpoint. port ( ) ,
154144 #[ cfg( feature = "tokio" ) ]
155145 client : bitreq:: Client :: new ( MAX_CONNECTIONS ) ,
156- } )
146+ }
157147 }
158148
159149 /// Sends a `GET` request for a resource identified by `uri`.
160150 ///
161151 /// Returns the response body in `F` format.
162152 #[ allow( dead_code) ]
163- pub async fn get < F > ( & mut self , uri : & str ) -> Result < F , HttpClientError >
153+ pub async fn get < F > ( & self , uri : & str ) -> Result < F , HttpClientError >
164154 where
165155 F : TryFrom < Vec < u8 > , Error = std:: io:: Error > ,
166156 {
167- let address = self . address ;
168- let response_body = self
169- . send_request_with_retry ( || {
170- let url = format ! ( "http://{}{}" , address, uri) ;
171- bitreq:: get ( url)
172- . with_timeout ( TCP_STREAM_RESPONSE_TIMEOUT . as_secs ( ) )
173- . with_max_body_size ( Some ( MAX_HTTP_MESSAGE_BODY_SIZE ) )
174- } )
175- . await ?;
157+ let url = format ! ( "http://{}:{}{}" , self . host, self . port, uri) ;
158+ let request = bitreq:: get ( url)
159+ . with_timeout ( TCP_STREAM_RESPONSE_TIMEOUT . as_secs ( ) )
160+ . with_max_body_size ( Some ( MAX_HTTP_MESSAGE_BODY_SIZE ) ) ;
161+ let response_body = self . send_request ( request) . await ?;
176162 F :: try_from ( response_body) . map_err ( HttpClientError :: Io )
177163 }
178164
@@ -183,57 +169,22 @@ impl HttpClient {
183169 /// format.
184170 #[ allow( dead_code) ]
185171 pub async fn post < F > (
186- & mut self , uri : & str , auth : & str , content : serde_json:: Value ,
172+ & self , uri : & str , auth : & str , content : serde_json:: Value ,
187173 ) -> Result < F , HttpClientError >
188174 where
189175 F : TryFrom < Vec < u8 > , Error = std:: io:: Error > ,
190176 {
191- let address = self . address ;
192- let content = content. to_string ( ) ;
193- let response_body = self
194- . send_request_with_retry ( || {
195- let url = format ! ( "http://{}{}" , address, uri) ;
196- bitreq:: post ( url)
197- . with_header ( "Authorization" , auth)
198- . with_header ( "Content-Type" , "application/json" )
199- . with_timeout ( TCP_STREAM_RESPONSE_TIMEOUT . as_secs ( ) )
200- . with_max_body_size ( Some ( MAX_HTTP_MESSAGE_BODY_SIZE ) )
201- . with_body ( content. clone ( ) )
202- } )
203- . await ?;
177+ let url = format ! ( "http://{}:{}{}" , self . host, self . port, uri) ;
178+ let request = bitreq:: post ( url)
179+ . with_header ( "Authorization" , auth)
180+ . with_header ( "Content-Type" , "application/json" )
181+ . with_timeout ( TCP_STREAM_RESPONSE_TIMEOUT . as_secs ( ) )
182+ . with_max_body_size ( Some ( MAX_HTTP_MESSAGE_BODY_SIZE ) )
183+ . with_body ( content. to_string ( ) ) ;
184+ let response_body = self . send_request ( request) . await ?;
204185 F :: try_from ( response_body) . map_err ( HttpClientError :: Io )
205186 }
206187
207- /// Sends an HTTP request message and reads the response, returning its body. Attempts to
208- /// reconnect and retry only on transport failures (not on HTTP errors like 500/404).
209- async fn send_request_with_retry (
210- & mut self , build_request : impl Fn ( ) -> bitreq:: Request ,
211- ) -> Result < Vec < u8 > , HttpClientError > {
212- match self . send_request ( build_request ( ) ) . await {
213- Ok ( bytes) => Ok ( bytes) ,
214- Err ( HttpClientError :: Http ( e) ) => {
215- // Don't retry on HTTP errors (non-2xx responses)
216- Err ( HttpClientError :: Http ( e) )
217- } ,
218- Err ( HttpClientError :: Io ( e) ) => {
219- // Don't retry on I/O errors (e.g., response parsing failures).
220- Err ( HttpClientError :: Io ( e) )
221- } ,
222- Err ( HttpClientError :: Transport ( _) ) => {
223- // Reconnect and retry on transport failures. This can happen if the connection
224- // was closed after the keep-alive limits are reached, or generally if the
225- // request timed out due to Bitcoin Core being stuck on a long-running operation
226- // or its RPC queue being full.
227- #[ cfg( feature = "tokio" ) ]
228- tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
229- #[ cfg( not( feature = "tokio" ) ) ]
230- std:: thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
231- * self = Self :: connect ( self . address ) ?;
232- self . send_request ( build_request ( ) ) . await
233- } ,
234- }
235- }
236-
237188 /// Sends an HTTP request message and reads the response, returning its body.
238189 async fn send_request ( & self , request : bitreq:: Request ) -> Result < Vec < u8 > , HttpClientError > {
239190 #[ cfg( feature = "tokio" ) ]
@@ -446,12 +397,12 @@ pub(crate) mod client_tests {
446397 }
447398 }
448399
449- #[ test]
450- fn connect_with_no_socket_address ( ) {
451- match HttpClient :: connect ( & vec ! [ ] [ .. ] ) {
452- Err ( HttpClientError :: Io ( e ) ) => {
453- assert_eq ! ( e . kind ( ) , std :: io :: ErrorKind :: InvalidInput )
454- } ,
400+ #[ tokio :: test]
401+ async fn connect_with_invalid_host ( ) {
402+ let endpoint = HttpEndpoint :: for_host ( "invalid.host.example" . to_string ( ) ) . with_port ( 80 ) ;
403+ let client = HttpClient :: new ( & endpoint ) ;
404+ match client . get :: < JsonResponse > ( "/foo" ) . await {
405+ Err ( HttpClientError :: Transport ( _ ) ) => { } ,
455406 Err ( e) => panic ! ( "Unexpected error type: {:?}" , e) ,
456407 Ok ( _) => panic ! ( "Expected error" ) ,
457408 }
@@ -461,7 +412,7 @@ pub(crate) mod client_tests {
461412 async fn read_error ( ) {
462413 let server = HttpServer :: responding_with_server_error ( "foo" ) ;
463414
464- let mut client = HttpClient :: connect ( & server. endpoint ( ) ) . unwrap ( ) ;
415+ let client = HttpClient :: new ( & server. endpoint ( ) ) ;
465416 match client. get :: < JsonResponse > ( "/foo" ) . await {
466417 Err ( HttpClientError :: Http ( http_error) ) => {
467418 assert_eq ! ( http_error. status_code, 500 ) ;
@@ -478,7 +429,7 @@ pub(crate) mod client_tests {
478429 let content = MessageBody :: Content ( body. clone ( ) ) ;
479430 let server = HttpServer :: responding_with_ok :: < String > ( content) ;
480431
481- let mut client = HttpClient :: connect ( & server. endpoint ( ) ) . unwrap ( ) ;
432+ let client = HttpClient :: new ( & server. endpoint ( ) ) ;
482433 match client. get :: < BinaryResponse > ( "/foo" ) . await {
483434 Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
484435 Ok ( bytes) => assert_eq ! ( bytes. 0 , body. as_bytes( ) ) ,
@@ -489,7 +440,7 @@ pub(crate) mod client_tests {
489440 async fn reconnect_closed_connection ( ) {
490441 let server = HttpServer :: responding_with_ok :: < String > ( MessageBody :: Empty ) ;
491442
492- let mut client = HttpClient :: connect ( & server. endpoint ( ) ) . unwrap ( ) ;
443+ let client = HttpClient :: new ( & server. endpoint ( ) ) ;
493444 assert ! ( client. get:: <BinaryResponse >( "/foo" ) . await . is_ok( ) ) ;
494445 match client. get :: < BinaryResponse > ( "/foo" ) . await {
495446 Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
0 commit comments