77/// Rate limiting is server-authoritative: the client surfaces `x-ratelimit-*`
88/// headers in error responses so agents can adapt their behaviour.
99use std:: env;
10- use std:: time:: Duration ;
10+ use std:: sync:: Arc ;
11+ use std:: time:: { Duration , Instant } ;
1112
1213use reqwest:: header:: { HeaderMap , HeaderValue } ;
1314use serde_json:: Value ;
@@ -64,14 +65,28 @@ pub struct BitmexClient {
6465 pub ( crate ) base_url : String ,
6566 pub ( crate ) testnet : bool ,
6667 verbose : bool ,
68+ /// When Some, timing phases are collected and printed for every request.
69+ timing : Option < super :: timing:: TimingHandle > ,
6770}
6871
69- fn build_http_client ( ) -> Result < reqwest:: Client > {
72+ fn build_http_client (
73+ timing : Option < & super :: timing:: TimingHandle > ,
74+ ) -> Result < reqwest:: Client > {
7075 let mut builder = reqwest:: Client :: builder ( )
7176 . use_rustls_tls ( )
7277 . timeout ( Duration :: from_secs ( 30 ) )
7378 . user_agent ( concat ! ( "bitmex-cli/" , env!( "CARGO_PKG_VERSION" ) ) ) ;
7479
80+ if let Some ( handle) = timing {
81+ builder = builder
82+ . dns_resolver ( Arc :: new (
83+ super :: timing:: TimingDnsResolver :: new ( handle. clone ( ) )
84+ ) )
85+ . connector_layer (
86+ super :: timing:: TimingConnectorLayer :: new ( handle. clone ( ) )
87+ ) ;
88+ }
89+
7590 if let Ok ( val) = env:: var ( "BITMEX_DANGER_ACCEPT_INVALID_CERTS" ) {
7691 if matches ! ( val. as_str( ) , "1" | "true" | "yes" ) {
7792 eprintln ! (
@@ -89,45 +104,44 @@ fn build_http_client() -> Result<reqwest::Client> {
89104
90105impl BitmexClient {
91106 /// Create a new client targeting mainnet or testnet.
92- pub fn new ( testnet : bool , verbose : bool ) -> Result < Self > {
107+ pub fn new ( testnet : bool , verbose : bool , timing : bool ) -> Result < Self > {
93108 let base_url = if testnet {
94109 TESTNET_URL . to_string ( )
95110 } else {
96111 MAINNET_URL . to_string ( )
97112 } ;
113+ let timing_handle = timing. then ( super :: timing:: new_handle) ;
98114 Ok ( Self {
99- http : build_http_client ( ) ?,
115+ http : build_http_client ( timing_handle . as_ref ( ) ) ?,
100116 base_url,
101117 testnet,
102118 verbose,
119+ timing : timing_handle,
103120 } )
104121 }
105122
106123 /// Create a new client with an explicit base URL override.
107- pub fn new_with_url ( base_url : String , testnet : bool , verbose : bool ) -> Result < Self > {
124+ pub fn new_with_url ( base_url : String , testnet : bool , verbose : bool , timing : bool ) -> Result < Self > {
125+ let timing_handle = timing. then ( super :: timing:: new_handle) ;
108126 Ok ( Self {
109- http : build_http_client ( ) ?,
127+ http : build_http_client ( timing_handle . as_ref ( ) ) ?,
110128 base_url,
111129 testnet,
112130 verbose,
131+ timing : timing_handle,
113132 } )
114133 }
115134
116135 /// Public GET — no authentication headers.
117136 pub async fn get ( & self , path : & str , query : & str ) -> Result < Value > {
118137 let url = self . url ( path, query) ;
119- if self . verbose {
120- crate :: cli:: output:: verbose ( & format ! ( "GET {url}" ) ) ;
121- }
138+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "GET {url}" ) ) ; }
139+ self . begin_request ( ) ;
122140 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
123- self . http
124- . get ( & url)
125- . send ( )
126- . await
141+ self . http . get ( & url) . send ( ) . await
127142 . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
128- } )
129- . await ?;
130- self . parse_response ( resp) . await
143+ } ) . await ?;
144+ self . parse_response ( resp, "GET" , & url) . await
131145 }
132146
133147 /// Authenticated GET.
@@ -138,97 +152,73 @@ impl BitmexClient {
138152 format ! ( "/api/v1{path}?{query}" )
139153 } ;
140154 let url = self . url ( path, query) ;
141- if self . verbose {
142- crate :: cli:: output:: verbose ( & format ! ( "GET {url} (auth)" ) ) ;
143- }
155+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "GET {url} (auth)" ) ) ; }
156+ self . begin_request ( ) ;
144157 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
145158 let expires = auth:: generate_expires ( ) ?;
146159 let sig = auth:: sign ( "GET" , & full_path, expires, "" , & creds. api_secret ) ?;
147160 let headers = self . auth_headers ( & creds. api_key , expires, & sig) ?;
148- self . http
149- . get ( & url)
150- . headers ( headers)
151- . send ( )
152- . await
161+ self . http . get ( & url) . headers ( headers) . send ( ) . await
153162 . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
154- } )
155- . await ?;
156- self . parse_response ( resp) . await
163+ } ) . await ?;
164+ self . parse_response ( resp, "GET" , & url) . await
157165 }
158166
159167 /// Authenticated POST with JSON body.
160168 pub async fn post ( & self , path : & str , body : & Value , creds : & Credentials ) -> Result < Value > {
161169 let full_path = format ! ( "/api/v1{path}" ) ;
162170 let body_str = serde_json:: to_string ( body) ?;
163171 let url = self . url ( path, "" ) ;
164- if self . verbose {
165- crate :: cli:: output:: verbose ( & format ! ( "POST {url}" ) ) ;
166- }
172+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "POST {url}" ) ) ; }
173+ self . begin_request ( ) ;
167174 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
168175 let expires = auth:: generate_expires ( ) ?;
169176 let sig = auth:: sign ( "POST" , & full_path, expires, & body_str, & creds. api_secret ) ?;
170177 let headers = self . auth_headers ( & creds. api_key , expires, & sig) ?;
171- self . http
172- . post ( & url)
173- . headers ( headers)
178+ self . http . post ( & url) . headers ( headers)
174179 . header ( "Content-Type" , "application/json" )
175- . body ( body_str. clone ( ) )
176- . send ( )
177- . await
180+ . body ( body_str. clone ( ) ) . send ( ) . await
178181 . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
179- } )
180- . await ?;
181- self . parse_response ( resp) . await
182+ } ) . await ?;
183+ self . parse_response ( resp, "POST" , & url) . await
182184 }
183185
184186 /// Authenticated PUT with JSON body.
185187 pub async fn put ( & self , path : & str , body : & Value , creds : & Credentials ) -> Result < Value > {
186188 let full_path = format ! ( "/api/v1{path}" ) ;
187189 let body_str = serde_json:: to_string ( body) ?;
188190 let url = self . url ( path, "" ) ;
189- if self . verbose {
190- crate :: cli:: output:: verbose ( & format ! ( "PUT {url}" ) ) ;
191- }
191+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "PUT {url}" ) ) ; }
192+ self . begin_request ( ) ;
192193 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
193194 let expires = auth:: generate_expires ( ) ?;
194195 let sig = auth:: sign ( "PUT" , & full_path, expires, & body_str, & creds. api_secret ) ?;
195196 let headers = self . auth_headers ( & creds. api_key , expires, & sig) ?;
196- self . http
197- . put ( & url)
198- . headers ( headers)
197+ self . http . put ( & url) . headers ( headers)
199198 . header ( "Content-Type" , "application/json" )
200- . body ( body_str. clone ( ) )
201- . send ( )
202- . await
199+ . body ( body_str. clone ( ) ) . send ( ) . await
203200 . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
204- } )
205- . await ?;
206- self . parse_response ( resp) . await
201+ } ) . await ?;
202+ self . parse_response ( resp, "PUT" , & url) . await
207203 }
208204
209205 /// Authenticated PATCH with JSON body.
210206 pub async fn patch ( & self , path : & str , body : & Value , creds : & Credentials ) -> Result < Value > {
211207 let full_path = format ! ( "/api/v1{path}" ) ;
212208 let body_str = serde_json:: to_string ( body) ?;
213209 let url = self . url ( path, "" ) ;
214- if self . verbose {
215- crate :: cli:: output:: verbose ( & format ! ( "PATCH {url}" ) ) ;
216- }
210+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "PATCH {url}" ) ) ; }
211+ self . begin_request ( ) ;
217212 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
218213 let expires = auth:: generate_expires ( ) ?;
219214 let sig = auth:: sign ( "PATCH" , & full_path, expires, & body_str, & creds. api_secret ) ?;
220215 let headers = self . auth_headers ( & creds. api_key , expires, & sig) ?;
221- self . http
222- . patch ( & url)
223- . headers ( headers)
216+ self . http . patch ( & url) . headers ( headers)
224217 . header ( "Content-Type" , "application/json" )
225- . body ( body_str. clone ( ) )
226- . send ( )
227- . await
218+ . body ( body_str. clone ( ) ) . send ( ) . await
228219 . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
229- } )
230- . await ?;
231- self . parse_response ( resp) . await
220+ } ) . await ?;
221+ self . parse_response ( resp, "PATCH" , & url) . await
232222 }
233223
234224 /// Authenticated DELETE, optional JSON body.
@@ -244,31 +234,21 @@ impl BitmexClient {
244234 } else {
245235 format ! ( "/api/v1{path}?{query}" )
246236 } ;
247- let body_str = body
248- . map ( |b| serde_json:: to_string ( b) )
249- . transpose ( ) ?
250- . unwrap_or_default ( ) ;
237+ let body_str = body. map ( |b| serde_json:: to_string ( b) ) . transpose ( ) ?. unwrap_or_default ( ) ;
251238 let url = self . url ( path, query) ;
252- if self . verbose {
253- crate :: cli:: output:: verbose ( & format ! ( "DELETE {url}" ) ) ;
254- }
239+ if self . verbose { crate :: cli:: output:: verbose ( & format ! ( "DELETE {url}" ) ) ; }
240+ self . begin_request ( ) ;
255241 let resp = super :: middleware:: execute_with_retry ( self . verbose , || async {
256242 let expires = auth:: generate_expires ( ) ?;
257- let sig =
258- auth:: sign ( "DELETE" , & full_path, expires, & body_str, & creds. api_secret ) ?;
243+ let sig = auth:: sign ( "DELETE" , & full_path, expires, & body_str, & creds. api_secret ) ?;
259244 let headers = self . auth_headers ( & creds. api_key , expires, & sig) ?;
260245 let mut req = self . http . delete ( & url) . headers ( headers) ;
261246 if !body_str. is_empty ( ) {
262- req = req
263- . header ( "Content-Type" , "application/json" )
264- . body ( body_str. clone ( ) ) ;
247+ req = req. header ( "Content-Type" , "application/json" ) . body ( body_str. clone ( ) ) ;
265248 }
266- req. send ( )
267- . await
268- . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
269- } )
270- . await ?;
271- self . parse_response ( resp) . await
249+ req. send ( ) . await . map_err ( |e| BitmexError :: Network { message : e. to_string ( ) } )
250+ } ) . await ?;
251+ self . parse_response ( resp, "DELETE" , & url) . await
272252 }
273253
274254 fn url ( & self , path : & str , query : & str ) -> String {
@@ -299,7 +279,28 @@ impl BitmexClient {
299279 Ok ( headers)
300280 }
301281
302- async fn parse_response ( & self , resp : reqwest:: Response ) -> Result < Value > {
282+ /// Reset timing state and record request_start. Call before each request.
283+ fn begin_request ( & self ) {
284+ if let Some ( ref handle) = self . timing {
285+ if let Ok ( mut phases) = handle. lock ( ) {
286+ * phases = super :: timing:: Phases {
287+ request_start : Some ( Instant :: now ( ) ) ,
288+ ..Default :: default ( )
289+ } ;
290+ }
291+ }
292+ }
293+
294+ /// Print timing summary to stderr. Call after body is fully read.
295+ fn end_request ( & self , method : & str , url : & str ) {
296+ if let Some ( ref handle) = self . timing {
297+ if let Ok ( phases) = handle. lock ( ) {
298+ super :: timing:: print_timing ( & phases, method, url) ;
299+ }
300+ }
301+ }
302+
303+ async fn parse_response ( & self , resp : reqwest:: Response , method : & str , url : & str ) -> Result < Value > {
303304 let status = resp. status ( ) ;
304305 let remaining = resp
305306 . headers ( )
@@ -312,8 +313,24 @@ impl BitmexClient {
312313 . and_then ( |v| v. to_str ( ) . ok ( ) )
313314 . and_then ( |s| s. parse :: < u64 > ( ) . ok ( ) ) ;
314315
316+ // Record TTFB — response headers have arrived.
317+ if let Some ( ref handle) = self . timing {
318+ if let Ok ( mut phases) = handle. lock ( ) {
319+ phases. ttfb = Some ( Instant :: now ( ) ) ;
320+ }
321+ }
322+
315323 let body = resp. text ( ) . await . map_err ( BitmexError :: from) ?;
316324
325+ // Record transfer end — body fully received. Print timing now so it
326+ // appears regardless of whether the response is a success or an error.
327+ if let Some ( ref handle) = self . timing {
328+ if let Ok ( mut phases) = handle. lock ( ) {
329+ phases. transfer_end = Some ( Instant :: now ( ) ) ;
330+ }
331+ }
332+ self . end_request ( method, url) ;
333+
317334 if self . verbose {
318335 crate :: cli:: output:: verbose ( & format ! ( "Status: {status}" ) ) ;
319336 if let Some ( r) = remaining {
@@ -348,6 +365,7 @@ impl BitmexClient {
348365
349366 let value: Value = serde_json:: from_str ( & body)
350367 . map_err ( |e| BitmexError :: Parse { message : format ! ( "Failed to parse response JSON: {e}" ) } ) ?;
368+
351369 Ok ( value)
352370 }
353371
0 commit comments