Skip to content

BF: Add timeout to follow_redirect HEAD requests; retry on Timeout#1849

Merged
yarikoptic merged 1 commit intomasterfrom
enh-redirect
Apr 30, 2026
Merged

BF: Add timeout to follow_redirect HEAD requests; retry on Timeout#1849
yarikoptic merged 1 commit intomasterfrom
enh-redirect

Conversation

@yarikoptic
Copy link
Copy Markdown
Member

Original motivator from me: we had windows keep failing here and there with getting stuck on redirects in test_follow_redirect and potentially others. So it made sense to add explicit timeout and retry on it.

claude summary of agreed upon changes:

follow_redirect() calls requests.head(url, allow_redirects=True) to resolve a redirect chain. No timeout was passed, so a stalled hop (e.g. a third-party shortener like bit.ly that accepts the TCP/TLS handshake but never sends a response) blocks the call indefinitely. This has been observed in test_follow_redirect on the Windows CI runner — pytest's fault-handler stack ends in socket.recv_into() -> ssl.read() inside requests.sessions.resolve_redirects.

The retry wrapper around requests.head() only caught requests.ConnectionError, which does not include requests.ReadTimeout — so even adding a timeout would have surfaced as a hard failure rather than the existing transient-error retry path.

Changes:

  • Add REDIRECT_HEAD_TIMEOUT = 30 constant in dandi/consts.py.
  • Pass it as timeout= to the requests.head() call.
  • Catch requests.Timeout in addition to requests.ConnectionError inside the retry loop, so ReadTimeout and ConnectTimeout are now retried with the same exponential backoff.
  • Widen the connection_error annotation to requests.RequestException | None to accommodate both exception types.
  • Add test_follow_redirect_retry_on_timeout parametrized over ReadTimeout and ConnectTimeout, mirroring the existing test_follow_redirect_retry_on_connection_error.
  • Update mock signatures in the existing tests to accept the new timeout= kwarg.

`follow_redirect()` calls `requests.head(url, allow_redirects=True)` to
resolve a redirect chain.  No timeout was passed, so a stalled hop (e.g.
a third-party shortener like bit.ly that accepts the TCP/TLS handshake
but never sends a response) blocks the call indefinitely.  This has been
observed in `test_follow_redirect` on the Windows CI runner — pytest's
fault-handler stack ends in `socket.recv_into() -> ssl.read()` inside
`requests.sessions.resolve_redirects`.

The retry wrapper around `requests.head()` only caught
`requests.ConnectionError`, which does not include `requests.ReadTimeout`
— so even adding a timeout would have surfaced as a hard failure rather
than the existing transient-error retry path.

Changes:
- Add `REDIRECT_HEAD_TIMEOUT = 30` constant in `dandi/consts.py`.
- Pass it as `timeout=` to the `requests.head()` call.
- Catch `requests.Timeout` in addition to `requests.ConnectionError`
  inside the retry loop, so `ReadTimeout` and `ConnectTimeout` are now
  retried with the same exponential backoff.
- Widen the `connection_error` annotation to
  `requests.RequestException | None` to accommodate both exception types.
- Add `test_follow_redirect_retry_on_timeout` parametrized over
  `ReadTimeout` and `ConnectTimeout`, mirroring the existing
  `test_follow_redirect_retry_on_connection_error`.
- Update mock signatures in the existing tests to accept the new
  `timeout=` kwarg.

Co-Authored-By: Claude Code 2.1.121 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yarikoptic yarikoptic added the patch Increment the patch version when merged label Apr 30, 2026
Comment thread dandi/dandiarchive.py
resp = requests.head(url, allow_redirects=True)
except requests.ConnectionError as e:
resp = requests.head(
url, allow_redirects=True, timeout=REDIRECT_HEAD_TIMEOUT
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I often use this myself when using the requests package - it is quite nice

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 95.83333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 76.29%. Comparing base (0fe03cf) to head (692dd07).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
dandi/consts.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1849      +/-   ##
==========================================
+ Coverage   76.26%   76.29%   +0.02%     
==========================================
  Files          87       87              
  Lines       12486    12505      +19     
==========================================
+ Hits         9523     9541      +18     
- Misses       2963     2964       +1     
Flag Coverage Δ
unittests 76.29% <95.83%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yarikoptic yarikoptic merged commit 5b272d7 into master Apr 30, 2026
42 of 43 checks passed
@yarikoptic yarikoptic deleted the enh-redirect branch April 30, 2026 17:16
@github-actions
Copy link
Copy Markdown

🚀 PR was released in 0.75.1 🚀

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

Labels

patch Increment the patch version when merged released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants