@@ -21,7 +21,6 @@ use std::sync::Arc;
2121use bytes:: Buf ;
2222use bytes:: Bytes ;
2323use http:: Request ;
24- use http:: StatusCode ;
2524use http:: header;
2625use serde:: Deserialize ;
2726
@@ -184,6 +183,7 @@ pub struct HfCore {
184183 pub root : String ,
185184 pub token : Option < String > ,
186185 pub endpoint : String ,
186+ pub max_retries : usize ,
187187
188188 #[ cfg( feature = "xet" ) ]
189189 pub xet_enabled : bool ,
@@ -238,17 +238,42 @@ impl HfCore {
238238 ///
239239 /// Returns the response parts (status, headers, etc.) alongside the
240240 /// deserialized body so callers can inspect headers when needed.
241+ ///
242+ /// When `max_retries` > 1, retries on commit conflicts (HTTP 412) and
243+ /// transient server errors (HTTP 5xx), matching the behavior of the
244+ /// official HuggingFace Hub client.
241245 async fn send_request < T : serde:: de:: DeserializeOwned > (
242246 & self ,
243247 req : Request < Buffer > ,
248+ max_retries : usize ,
244249 ) -> Result < ( http:: response:: Parts , T ) > {
245- let resp = self . info . http_client ( ) . send ( req) . await ?;
246- if !resp. status ( ) . is_success ( ) {
247- return Err ( parse_error ( resp) ) ;
250+ let client = self . info . http_client ( ) ;
251+ let mut attempt = 0 ;
252+ loop {
253+ match client. send ( req. clone ( ) ) . await {
254+ Ok ( resp) if resp. status ( ) . is_success ( ) => {
255+ let ( parts, body) = resp. into_parts ( ) ;
256+ let parsed = serde_json:: from_reader ( body. reader ( ) )
257+ . map_err ( new_json_deserialize_error) ?;
258+ return Ok ( ( parts, parsed) ) ;
259+ }
260+ Ok ( resp) => {
261+ attempt += 1 ;
262+ let err = parse_error ( resp) ;
263+ let retryable =
264+ err. kind ( ) == ErrorKind :: ConditionNotMatch || err. is_temporary ( ) ;
265+ if attempt >= max_retries || !retryable {
266+ return Err ( err) ;
267+ }
268+ }
269+ Err ( err) => {
270+ attempt += 1 ;
271+ if attempt >= max_retries || !err. is_temporary ( ) {
272+ return Err ( err) ;
273+ }
274+ }
275+ }
248276 }
249- let ( parts, body) = resp. into_parts ( ) ;
250- let parsed = serde_json:: from_reader ( body. reader ( ) ) . map_err ( new_json_deserialize_error) ?;
251- Ok ( ( parts, parsed) )
252277 }
253278
254279 pub async fn path_info ( & self , path : & str ) -> Result < PathInfo > {
@@ -261,7 +286,7 @@ impl HfCore {
261286 . header ( header:: CONTENT_TYPE , "application/x-www-form-urlencoded" )
262287 . body ( Buffer :: from ( Bytes :: from ( form_body) ) )
263288 . map_err ( new_request_build_error) ?;
264- let ( _, mut files) = self . send_request :: < Vec < PathInfo > > ( req) . await ?;
289+ let ( _, mut files) = self . send_request :: < Vec < PathInfo > > ( req, 1 ) . await ?;
265290
266291 // NOTE: if the file is not found, the server will return 200 with an empty array
267292 if files. is_empty ( ) {
@@ -284,7 +309,7 @@ impl HfCore {
284309 . request ( http:: Method :: GET , & url, Operation :: List )
285310 . body ( Buffer :: new ( ) )
286311 . map_err ( new_request_build_error) ?;
287- let ( parts, files) = self . send_request :: < Vec < PathInfo > > ( req) . await ?;
312+ let ( parts, files) = self . send_request :: < Vec < PathInfo > > ( req, 1 ) . await ?;
288313
289314 let next_cursor = parts
290315 . headers
@@ -302,16 +327,17 @@ impl HfCore {
302327 . request ( http:: Method :: GET , & url, Operation :: Read )
303328 . body ( Buffer :: new ( ) )
304329 . map_err ( new_request_build_error) ?;
305- let ( _, token) = self . send_request ( req) . await ?;
330+ let ( _, token) = self . send_request ( req, 1 ) . await ?;
306331 Ok ( token)
307332 }
308333
309334 /// Issue a HEAD request and extract XET file info (hash and size).
310335 ///
311- /// Uses a custom HTTP client that does NOT follow redirects so we can
312- /// inspect response headers (e.g. `X-Xet-Hash`) from the 302 response.
313- ///
314336 /// Returns `None` if the `X-Xet-Hash` header is absent or empty.
337+ ///
338+ /// NOTE: Cannot use `send_request` here because we need a custom
339+ /// no-redirect HTTP client to inspect headers (e.g. `X-Xet-Hash`)
340+ /// from the 302 response, and the response is not JSON.
315341 #[ cfg( feature = "xet" ) ]
316342 pub ( super ) async fn get_xet_file ( & self , path : & str ) -> Result < Option < XetFile > > {
317343 let uri = self . repo . uri ( & self . root , path) ;
@@ -330,7 +356,17 @@ impl HfCore {
330356 . body ( Buffer :: new ( ) )
331357 . map_err ( new_request_build_error) ?;
332358
333- let resp = client. send ( req) . await ?;
359+ // Retry on transient errors, same as send_request.
360+ let mut attempt = 0 ;
361+ let resp = loop {
362+ let resp = client. send ( req. clone ( ) ) . await ?;
363+
364+ attempt += 1 ;
365+ let retryable = resp. status ( ) . is_server_error ( ) ;
366+ if attempt >= self . max_retries || !retryable {
367+ break resp;
368+ }
369+ } ;
334370
335371 let hash = resp
336372 . headers ( )
@@ -385,11 +421,15 @@ impl HfCore {
385421 . body ( Buffer :: from ( json_body) )
386422 . map_err ( new_request_build_error) ?;
387423
388- let ( _, resp) = self . send_request ( req) . await ?;
424+ let ( _, resp) = self . send_request ( req, 1 ) . await ?;
389425 Ok ( resp)
390426 }
391427
392428 /// Commit file changes (uploads and/or deletions) to the repository.
429+ ///
430+ /// Retries on commit conflicts (HTTP 412) and transient server errors
431+ /// (HTTP 5xx), matching the behavior of the official HuggingFace Hub
432+ /// client.
393433 pub ( super ) async fn commit_files (
394434 & self ,
395435 regular_files : Vec < CommitFile > ,
@@ -411,7 +451,6 @@ impl HfCore {
411451 . or_else ( || deleted_files. first ( ) . map ( |f| f. path . as_str ( ) ) )
412452 . ok_or_else ( || Error :: new ( ErrorKind :: Unexpected , "no files to commit" ) ) ?;
413453
414- let client = self . info . http_client ( ) ;
415454 let uri = self . repo . uri ( & self . root , first_path) ;
416455 let url = uri. commit_url ( & self . endpoint ) ;
417456
@@ -431,11 +470,9 @@ impl HfCore {
431470 . body ( Buffer :: from ( json_body) )
432471 . map_err ( new_request_build_error) ?;
433472
434- let resp = client. send ( req) . await ?;
435- match resp. status ( ) {
436- StatusCode :: OK | StatusCode :: CREATED => Ok ( ( ) ) ,
437- _ => Err ( parse_error ( resp) ) ,
438- }
473+ self . send_request :: < serde_json:: Value > ( req, self . max_retries )
474+ . await ?;
475+ Ok ( ( ) )
439476 }
440477}
441478
@@ -537,6 +574,7 @@ pub(crate) mod test_utils {
537574 root : "/" . to_string ( ) ,
538575 token : None ,
539576 endpoint : endpoint. to_string ( ) ,
577+ max_retries : 3 ,
540578 #[ cfg( feature = "xet" ) ]
541579 xet_enabled : false ,
542580 } ;
0 commit comments