pypa/pip · error · IncompleteDownloadError

incomplete-download

incomplete-download

Error message

Download failed because not enough bytes were received ({download_status})

What it means

Raised as IncompleteDownloadError inside _attempt_resumes_or_redownloads when, during a resume request, the server's 206 'Content-Range' header start byte does not equal the bytes already received. The server is misbehaving (returning a range at an unexpected offset), so pip aborts rather than write a corrupted file. Distinct from the exhaustion case: this fires on the first mismatched resume before retries run out.

Solutions

  1. Retry the install; transient mid-stream mismatches often clear on a fresh download.
  2. Disable resume retries for this mirror (--resume-retries 0) so pip restarts cleanly instead of resuming.
  3. Switch to an index/mirror that honours HTTP Range requests correctly (e.g. PyPI directly).
  4. Clear the pip cache (pip cache purge) in case a stale partial is being reused.
Defensive patterns

Strategy: retry

Try / catch

from pip._internal.exceptions import IncompleteDownloadError
for _ in range(3):
    try:
        run_pip_install(["pkg"])
        break
    except IncompleteDownloadError:
        continue

Prevention

When it happens

Trigger: A resumable download is retried (--resume-retries>0); the server returns 206 Partial Content whose Content-Range 'bytes START-...' START differs from download.bytes_received (e.g. server restarted from 0, or rewrote the range).

Common situations: Misconfigured CDN or caching proxy that ignores or rewrites Range/Content-Range; mirrors that don't honour byte ranges; load balancers shuffling resume requests to a different backend with a different file.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/a9bff1440165fbed. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_internal/network/download.py:279

            try:
                resume_resp = self._http_get_resume(download, should_match=first_resp)
                # Fallback: if the server responded with 200 (i.e., the file has
                # since been modified or range requests are unsupported) or any
                # other unexpected status, restart the download from the beginning.
                must_restart = resume_resp.status_code != HTTPStatus.PARTIAL_CONTENT
                if must_restart:
                    download.reset_file()
                    download.size = _get_http_response_size(resume_resp)
                    first_resp = resume_resp
                else:
                    # If the resume request starts at the wrong location, fail
                    # outright since the server is misbehaving.
                    content_range = resume_resp.headers.get("Content-Range", "")
                    resumed_at = content_range.lower().partition("bytes ")[2]
                    resumed_at = resumed_at.partition("-")[0]
                    if resumed_at and resumed_at != str(download.bytes_received):
                        raise IncompleteDownloadError(download)

                self._process_response(download, resume_resp)
            except (
                ConnectionFailedError,
                ConnectionTimeoutError,
                ProxyConnectionError,
                SSLVerificationError,
                ReadTimeoutError,
                ProtocolError,
                OSError,
            ):
                # The error handling here is tricky, a few notes:
                #
                # - The diagnostic connection errors are raised by our custom
                #   requests.request() connection exception handler.
                # - ProtocolError is raised by urllib3 when the returned data length
                #   doesn't match the response Content-Length.
                # - ReadTimeoutError / ProtocolError come straight from urllib3 (via

View on GitHub (pinned to f399c37189)