fix(cli): wait out rate limits during a WebDAV transfer - #1532
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
A Team workspace push or pull walks a project with one PROPFIND per directory, then moves one file per request. The service meters every one of those, so a transfer of any real size meets its own rate limit. Each request site called raise_for_status() directly, so the first 429 aborted the whole transfer. Retrying is what makes progress possible at all. Because every re-run restarts the walk at the first directory, a project past the per-minute ceiling could never finish no matter how long the user waited: each attempt spent a fresh window re-listing directories it already had and died at the same depth. Route all three request sites (PROPFIND, GET, PUT) through a helper that honors Retry-After and repeats the identical request. Waits are floored at one second so an immediate retry cannot spin and capped at sixty so an implausible header cannot hang the CLI; an absent or unparseable value falls back to a fixed wait, since a header we cannot read still tells us the window is closed. The body is passed explicitly rather than forwarded, so a caller cannot add a parameter that makes a retry into a different request. A create-only 412 is still this call's answer and is not retried. _describe() now appends the rate-limit headers to a 429. The response body alone does not say what the limit was or when it resets, which left those headers reachable only by editing this file. Diagnosed by a customer running 0.23.2, who traced it to _propfind() and verified that Retry-After handling let the pull complete. Refs basicmachines-co/basic-memory-cloud#2039 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
phernandez
force-pushed
the
fix-2039-webdav-rate-limit-retry
branch
from
September 10, 2026 16:34
7a74968 to
b788bff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Reported by a customer on 0.23.2:
bm cloud pull --name cabinetagainst a Team workspace failed repeatedly withA Team transfer runs over WebDAV (#1262).
list_project_fileswalks the project with oneDepth: 1PROPFIND per directory, andwebdav_project_diffruns that walk for both push and pull. The service meters every request on this transport, so a transfer of any real size meets its own rate limit as a matter of course.All three request sites called
raise_for_status()directly, so the first 429 anywhere aborted the entire transfer.The part that made it unrecoverable rather than merely slow: each re-run restarts the walk at the first directory. Every attempt spent a fresh window re-listing directories it already had, then died at about the same depth. Waiting did not help, so a project past the per-minute ceiling could never complete. The rate-limit headers were accurate the whole time; there was simply no way to make forward progress.
The customer confirmed the diagnosis by patching
_propfind()locally to honorRetry-After, after which the pull completed.What
Route all three request sites through
_request_with_rate_limit_retry:_propfinddownload_fileupload_fileOnly PROPFIND was in the original report, but a pull that survives enumeration then issues one GET per file, so fixing the walk alone would have moved the failure into the transfer phase.
Retry-Afteris read as either delay-seconds or an HTTP-date. Waits are floored at 1s so a "retry immediately" answer cannot spin, capped at 60s so an implausible header cannot hang the CLI, and an absent or unparseable value falls back to a fixed wait, since a header we cannot read still tells us the window is closed. Attempts are bounded at 6; the last 429 is returned rather than raised so the caller's existing error path reports it._describe()now appends the rate-limit headers to a 429. The body alone does not say what the limit was or when it resets, which is why the customer had to edit this file to see them.Safety of replaying
Every request here is safe to repeat. The reads carry no caller-supplied body, and the one write sends the same bytes and the same
X-OC-Mtimeeach time, so a replay is the identical request rather than a second effect. The body is passed explicitly rather than forwarded as**kwargs, so a caller cannot later add a parameter that quietly makes a retry into a different request.A create-only
412is untouched: it is the answer that call asked for, not a rejection to wait out, and it is still checked beforeraise_for_status().Testing
Eight new tests in
tests/cli/cloud/test_webdav_client.py, using the existingMockTransportharness with_sleeppatched so the waits are asserted rather than spent:If-None-Match: *limit,remaining,retry afterandresets atin the errorRetry-Afterparsing across delay-seconds,0(floored), an implausible value (capped), absent, unparseable, and a past HTTP-dateVerified both guards are load-bearing:
_RATE_LIMIT_MAX_ATTEMPTS = 1fails exactly the four survival tests_describe()to drop the detail fails exactlytest_persistent_rate_limit_reports_the_headersRestored after each.
tests/cli/cloud/passes in full (175 tests) andjust checkexits 0.Scope
This makes a large Team transfer possible, not fast. It still costs one request per directory plus one per file, so a big project spends real time waiting out windows. Two related pieces are tracked in basicmachines-co/basic-memory-cloud#2039:
Depthrequest header, so the client cannot collapse the walk into a single recursive PROPFIND. That is the change that would make large projects fast.Credit
The diagnosis, the pointer to
_propfind()and_describe(), and the confirmation thatRetry-Afterhandling resolves it all came from a customer report, and the reporter offereda patch. Named credit is theirs to claim rather than ours to publish; happy to add it here on
request.
🤖 Generated with Claude Code