Skip to content

fix(nodeenv): retry broken node downloads and stop hanging on stalls - #428

Merged
ekalinin merged 1 commit into
masterfrom
fix/download-retries
Oct 4, 2026
Merged

ekalinin merged 1 commit into
masterfrom
fix/download-retries

Conversation

@ekalinin

@ekalinin ekalinin commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Fixes #324

What happens

The 'bytes' object has no attribute 'tell' from the report is gone since #329, which retries IncompleteRead up to 3 times. Checked against a local server that breaks the download in different ways, master still:

  • gives up with a raw IncompleteRead traceback once all 3 attempts fail, and makes them back to back;
  • does not retry a connection reset in the middle of the body at all (ConnectionResetError traceback after 1 request);
  • hangs forever when the server stops sending, since urlopen has no timeout.

Switching to requests, as the issue suggests, would not help: requests 2.34 / urllib3 2.8 raise ChunkedEncodingError on the same truncated, chunked and reset bodies. "Multipart" in the title is network truncation.

What changes

  • _download_node_file retries any OSError while reading the body as well, not only IncompleteRead. HTTPError is an OSError too, so it is re-raised first: install_node_wrapped() needs it for the arm64 -> x64 fallback.
  • The attempts are 3 seconds apart, and the last failure is reported as Error: cannot download <url>: <reason> with exit code 1.
  • _urlopen passes timeout=download_timeout (60 seconds) in all three branches. It is a timeout per socket operation, not for the whole download.
  • urllib wraps a timeout into URLError only while it sends the request; a timeout waiting for the answer comes out as a bare TimeoutError. urlopen() now reports socket.timeout the same way as an unreachable host, so --list against a stalled mirror ends with an error message too.

Not covered: a body without Content-Length that is cut short looks complete to HTTP and still fails later in tarfile with EOFError, and the npm tarball download in install_npm has no retries.

Tests

The new tests fail on master and pass with the fix:

  • a connection reset or timed out in the middle of the body is retried;
  • the last failure is reported and exits instead of raising (updated test__download_node_file);
  • the pause is between the attempts only;
  • urlopen() gives up on a server that accepts the connection and never answers, in each of the three _urlopen branches.

test__download_node_file_leaves_http_errors_to_the_caller passes on master; it guards the HTTPError re-raise, and fails if OSError is caught without it. TestCertifi::test_urlopen_without_certifi now checks that no context is passed, as its docstring says, instead of empty kwargs.

_download_node_file() retried IncompleteRead only, back to back, and
re-raised it as a traceback after the last attempt. A connection reset
or timed out in the middle of the body was not retried at all, and
urlopen() had no timeout, so a server that stopped sending hung
nodeenv forever.

Now any OSError while reading the body is retried as well, except
HTTPError, which install_node_wrapped() needs for the arm64 -> x64
fallback. The attempts are 3 seconds apart, and the last failure is
reported as "Error: cannot download <url>: <reason>" with exit code 1.

urllib now waits download_timeout (60) seconds for a server to answer
or to send more data. A timeout while waiting for the answer is not
wrapped into URLError, so urlopen() reports socket.timeout the same
way as an unreachable host.

Fixes #324
@ekalinin
ekalinin merged commit ecb4b74 into master Oct 4, 2026
46 checks passed
@bagerard

bagerard commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for the fix 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

download_node_src does not properly handle multipart downloads

2 participants