|
7 | 7 | import time |
8 | 8 | import uuid |
9 | 9 | import warnings |
| 10 | +from contextlib import ExitStack |
10 | 11 | from dataclasses import dataclass |
11 | 12 | from pathlib import Path |
12 | 13 | from typing import Any, BinaryIO, Literal, NoReturn, overload |
@@ -374,100 +375,103 @@ def http_get( |
374 | 375 | " Install `hf_xet` with `pip install hf_xet` for xet-powered downloads." |
375 | 376 | ) |
376 | 377 |
|
377 | | - with http_stream_backoff( |
378 | | - method="GET", |
379 | | - url=url, |
380 | | - headers=headers, |
381 | | - timeout=constants.HF_HUB_DOWNLOAD_TIMEOUT, |
382 | | - retry_on_exceptions=(), |
383 | | - retry_on_status_codes=(408, 429), |
384 | | - ) as response: |
385 | | - hf_raise_for_status(response) |
386 | | - |
387 | | - # If we requested a Range but got 200 back, the server ignored our Range header |
388 | | - # (e.g. CloudFront with Accept-Encoding: gzip). Reset file to avoid corruption. |
389 | | - if resume_size > 0 and response.status_code == 200: |
390 | | - temp_file.seek(0) |
391 | | - temp_file.truncate() |
392 | | - if _tqdm_bar is not None: |
393 | | - # When the progress bar is reused across retries, its counter has already been advanced by `resume_size` |
394 | | - # worth of chunks from earlier attempts. Those bytes are gone from disk now, so roll the counter back |
395 | | - # to keep the upcoming full re-download from double-counting (e.g. ending at 130/100 on a 100-byte file). |
396 | | - _tqdm_bar.update(-resume_size) |
397 | | - if callable(update_transfer := getattr(_tqdm_bar, "update_transfer", None)): |
398 | | - update_transfer(-resume_size) |
399 | | - resume_size = 0 |
400 | | - |
401 | | - total: int | None = _get_file_length_from_http_response(response) |
402 | | - if total is None: |
403 | | - # Hub serves compressible text files (e.g. vocab.json) with `Content-Encoding: gzip` and |
404 | | - # `Transfer-Encoding: chunked`, so the response carries no `Content-Length`. Fall back to the caller's |
405 | | - # `expected_size` (always known from the metadata HEAD on the hf_hub path) so the progress bar, and any |
406 | | - # aggregating wrapper such as snapshot_download's `_AggregatedTqdm` — still sees the file size. |
407 | | - total = expected_size |
408 | | - |
409 | | - if displayed_filename is None: |
410 | | - displayed_filename = url |
411 | | - content_disposition = response.headers.get("Content-Disposition") |
412 | | - if content_disposition is not None: |
413 | | - match = HEADER_FILENAME_PATTERN.search(content_disposition) |
414 | | - if match is not None: |
415 | | - # Means file is on CDN |
416 | | - displayed_filename = match.groupdict()["filename"] |
417 | | - |
418 | | - # Truncate filename if too long to display |
419 | | - if len(displayed_filename) > 40: |
420 | | - displayed_filename = f"(…){displayed_filename[-40:]}" |
421 | | - |
422 | | - consistency_error_message = ( |
423 | | - f"Consistency check failed: file should be of size {expected_size} but has size" |
424 | | - f" {{actual_size}} ({displayed_filename}).\nThis is usually due to network issues while downloading the file." |
425 | | - " Please retry with `force_download=True`." |
426 | | - ) |
427 | | - progress_cm = _get_progress_bar_context( |
428 | | - desc=displayed_filename, |
429 | | - log_level=logger.getEffectiveLevel(), |
430 | | - total=total, |
431 | | - initial=resume_size, |
432 | | - name="huggingface_hub.http_get", |
433 | | - tqdm_class=tqdm_class, |
434 | | - _tqdm_bar=_tqdm_bar, |
435 | | - ) |
436 | | - |
437 | | - with progress_cm as progress: |
438 | | - new_resume_size = resume_size |
439 | | - try: |
440 | | - for chunk in response.iter_bytes(chunk_size=constants.DOWNLOAD_CHUNK_SIZE): |
441 | | - if chunk: # filter out keep-alive new chunks |
442 | | - progress.update(len(chunk)) |
443 | | - if callable(update_transfer := getattr(progress, "update_transfer", None)): |
444 | | - update_transfer(len(chunk)) |
445 | | - temp_file.write(chunk) |
446 | | - new_resume_size += len(chunk) |
447 | | - # Some data has been downloaded from the server so we reset the number of retries. |
448 | | - _nb_retries = 5 |
449 | | - except (httpx.ConnectError, httpx.TimeoutException, httpx.RemoteProtocolError) as e: |
450 | | - # If ConnectionError (SSLError), ReadTimeout, or RemoteProtocolError (peer closed the connection before |
451 | | - # sending the complete body) happen while streaming data from the server, it is most likely a transient |
452 | | - # error (network outage?). We log a warning message and try to resume the download a few times before |
453 | | - # giving up. The retry mechanism is basic but should be enough in most cases. |
454 | | - if _nb_retries <= 0: |
455 | | - logger.warning("Error while downloading from %s: %s\nMax retries exceeded.", url, str(e)) |
456 | | - raise |
457 | | - logger.warning("Error while downloading from %s: %s\nTrying to resume download...", url, str(e)) |
458 | | - time.sleep(1) |
459 | | - return http_get( |
| 378 | + # Keep the response and progress bar open while recursive retries reuse them. |
| 379 | + with ExitStack() as stack: |
| 380 | + progress = _tqdm_bar |
| 381 | + new_resume_size = resume_size |
| 382 | + try: |
| 383 | + response = stack.enter_context( |
| 384 | + http_stream_backoff( |
| 385 | + method="GET", |
460 | 386 | url=url, |
461 | | - temp_file=temp_file, |
462 | | - resume_size=new_resume_size, |
463 | | - headers=initial_headers, |
464 | | - expected_size=expected_size, |
465 | | - tqdm_class=tqdm_class, |
466 | | - _nb_retries=_nb_retries - 1, |
467 | | - # Reuse the existing progress bar across retries so a custom `tqdm_class` (e.g. snapshot_download's `_AggregatedTqdm`, |
468 | | - # which mutates a shared parent bar in `__init__`) is not re-instantiated and does not double-count `total`/`initial`. |
469 | | - _tqdm_bar=progress, |
| 387 | + headers=headers, |
| 388 | + timeout=constants.HF_HUB_DOWNLOAD_TIMEOUT, |
| 389 | + retry_on_exceptions=(), |
| 390 | + retry_on_status_codes=(408, 429), |
470 | 391 | ) |
| 392 | + ) |
| 393 | + hf_raise_for_status(response) |
| 394 | + |
| 395 | + # If we requested a Range but got 200 back, the server ignored our Range header |
| 396 | + # (e.g. CloudFront with Accept-Encoding: gzip). Reset file to avoid corruption. |
| 397 | + if resume_size > 0 and response.status_code == 200: |
| 398 | + temp_file.seek(0) |
| 399 | + temp_file.truncate() |
| 400 | + if _tqdm_bar is not None: |
| 401 | + # When the progress bar is reused across retries, its counter has already been advanced by `resume_size` |
| 402 | + # worth of chunks from earlier attempts. Those bytes are gone from disk now, so roll the counter back |
| 403 | + # to keep the upcoming full re-download from double-counting (e.g. ending at 130/100 on a 100-byte file). |
| 404 | + _tqdm_bar.update(-resume_size) |
| 405 | + if callable(update_transfer := getattr(_tqdm_bar, "update_transfer", None)): |
| 406 | + update_transfer(-resume_size) |
| 407 | + resume_size = 0 |
| 408 | + |
| 409 | + total: int | None = _get_file_length_from_http_response(response) |
| 410 | + if total is None: |
| 411 | + # Hub serves compressible text files (e.g. vocab.json) with `Content-Encoding: gzip` and |
| 412 | + # `Transfer-Encoding: chunked`, so the response carries no `Content-Length`. Fall back to the caller's |
| 413 | + # `expected_size` (always known from the metadata HEAD on the hf_hub path) so the progress bar, and any |
| 414 | + # aggregating wrapper such as snapshot_download's `_AggregatedTqdm` — still sees the file size. |
| 415 | + total = expected_size |
| 416 | + |
| 417 | + if displayed_filename is None: |
| 418 | + displayed_filename = url |
| 419 | + content_disposition = response.headers.get("Content-Disposition") |
| 420 | + if content_disposition is not None: |
| 421 | + match = HEADER_FILENAME_PATTERN.search(content_disposition) |
| 422 | + if match is not None: |
| 423 | + # Means file is on CDN |
| 424 | + displayed_filename = match.groupdict()["filename"] |
| 425 | + |
| 426 | + # Truncate filename if too long to display |
| 427 | + if len(displayed_filename) > 40: |
| 428 | + displayed_filename = f"(…){displayed_filename[-40:]}" |
| 429 | + |
| 430 | + consistency_error_message = ( |
| 431 | + f"Consistency check failed: file should be of size {expected_size} but has size" |
| 432 | + f" {{actual_size}} ({displayed_filename}).\nThis is usually due to network issues while downloading the file." |
| 433 | + " Please retry with `force_download=True`." |
| 434 | + ) |
| 435 | + progress_cm = _get_progress_bar_context( |
| 436 | + desc=displayed_filename, |
| 437 | + log_level=logger.getEffectiveLevel(), |
| 438 | + total=total, |
| 439 | + initial=resume_size, |
| 440 | + name="huggingface_hub.http_get", |
| 441 | + tqdm_class=tqdm_class, |
| 442 | + _tqdm_bar=_tqdm_bar, |
| 443 | + ) |
| 444 | + |
| 445 | + progress = stack.enter_context(progress_cm) |
| 446 | + new_resume_size = resume_size |
| 447 | + for chunk in response.iter_bytes(chunk_size=constants.DOWNLOAD_CHUNK_SIZE): |
| 448 | + if chunk: # filter out keep-alive new chunks |
| 449 | + progress.update(len(chunk)) |
| 450 | + if callable(update_transfer := getattr(progress, "update_transfer", None)): |
| 451 | + update_transfer(len(chunk)) |
| 452 | + temp_file.write(chunk) |
| 453 | + new_resume_size += len(chunk) |
| 454 | + # Some data has been downloaded from the server so we reset the number of retries. |
| 455 | + _nb_retries = 5 |
| 456 | + except (httpx.ConnectError, httpx.TimeoutException, httpx.RemoteProtocolError) as e: |
| 457 | + # Retry transient failures both when opening the stream and while reading its body. |
| 458 | + if _nb_retries <= 0: |
| 459 | + logger.warning("Error while downloading from %s: %s\nMax retries exceeded.", url, str(e)) |
| 460 | + raise |
| 461 | + logger.warning("Error while downloading from %s: %s\nTrying to resume download...", url, str(e)) |
| 462 | + time.sleep(1) |
| 463 | + return http_get( |
| 464 | + url=url, |
| 465 | + temp_file=temp_file, |
| 466 | + resume_size=new_resume_size, |
| 467 | + headers=initial_headers, |
| 468 | + expected_size=expected_size, |
| 469 | + tqdm_class=tqdm_class, |
| 470 | + _nb_retries=_nb_retries - 1, |
| 471 | + # Reuse the existing progress bar across retries so a custom `tqdm_class` (e.g. snapshot_download's `_AggregatedTqdm`, |
| 472 | + # which mutates a shared parent bar in `__init__`) is not re-instantiated and does not double-count `total`/`initial`. |
| 473 | + _tqdm_bar=progress, |
| 474 | + ) |
471 | 475 |
|
472 | 476 | if expected_size is not None and expected_size != temp_file.tell(): |
473 | 477 | raise OSError( |
|
0 commit comments