Harden the retry classifier and pin per-attempt deadlines (closes #80)
check / check (push) Successful in 17s
check / check (push) Successful in 17s
A POST or PUT is replayed only when every errno in the cause chain is a connect errno and the walk reached the end of the chain, and postJSON/putJSON no longer follow redirects, so a redirect is an ApiError that is not retried. getRetryOptions() returns a copy. New tests pin every errno the classifier names, the cause-chain depth limit, a chain deeper than the limit, a two-error cycle, and a fresh deadline per attempt for every retrying entry point. The README endpoint list is now the one place naming the requests the replay rule covers; code comments point to it. Model: opus-5-5
This commit is contained in:
@@ -323,6 +323,7 @@ Endpoints used:
|
||||
- `GET /collections/v2/diff?collectionID=<id>&sinceTime=<usec>`: list files in a
|
||||
collection; paginate while `hasMore` is true.
|
||||
- `GET https://files.ente.io/?fileID=<id>`: download encrypted file bytes.
|
||||
- `POST /files/data/fetch`: fetch encrypted ML data for a batch of files.
|
||||
- `POST /files/upload-url`: mint a presigned upload URL (for thumbnail repair).
|
||||
- `PUT /files/thumbnail`: register an uploaded thumbnail's object key.
|
||||
|
||||
@@ -377,20 +378,23 @@ headers — `getFileStream` returns as soon as headers arrive, so a deadline tha
|
||||
only guarded the initial request would leave the same hang one layer down.
|
||||
|
||||
**Non-idempotent requests are not blindly replayed.** `postJSON` and `putJSON`
|
||||
reach `/users/srp/create-session`, `/users/two-factor/verify` — which consumes
|
||||
one of a small number of second-factor attempts — and `/files/thumbnail`. They
|
||||
are retried only on the three failures that establish no TCP connection to the
|
||||
server ever existed, so no request byte can have been transmitted: `ENOTFOUND`
|
||||
and `EAI_AGAIN` (name resolution produced no address) and `ECONNREFUSED` (the
|
||||
peer refused the connection). A 5xx, a mid-flight reset and a deadline are all
|
||||
left to the caller, because each of them can happen after the server has already
|
||||
acted. The routing errnos `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are
|
||||
excluded for the same reason, despite looking like connect-time failures: on
|
||||
Linux an ICMP unreachable arriving mid-flight, or a local interface going down
|
||||
after the request was written, delivers them on an already-established socket.
|
||||
They stay retryable for the idempotent calls. `putFile` is exempt: a presigned
|
||||
PUT stores one whole object at one key in one request, so replaying it has no
|
||||
partial state to damage.
|
||||
send every `POST` and `PUT` in the endpoint list above; some of them change
|
||||
server state, and `/users/two-factor/verify` consumes one of a small number of
|
||||
second-factor attempts. They are retried only when every errno in the error's
|
||||
`cause` chain is one of the three that establish no TCP connection to the server
|
||||
ever existed, so no request byte can have been transmitted: `ENOTFOUND` and
|
||||
`EAI_AGAIN` (name resolution produced no address) and `ECONNREFUSED` (the peer
|
||||
refused the connection). A 5xx, a mid-flight reset and a deadline are all left
|
||||
to the caller, because each of them can happen after the server has already
|
||||
acted. These two do not follow redirects either: a redirect means the server
|
||||
already received the request, so it is reported as an error and not retried. The
|
||||
routing errnos `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are excluded for the
|
||||
same reason, despite looking like connect-time failures: on Linux an ICMP
|
||||
unreachable arriving mid-flight, or a local interface going down after the
|
||||
request was written, delivers them on an already-established socket. They stay
|
||||
retryable for the idempotent calls. `putFile` is exempt: a presigned PUT stores
|
||||
one whole object at one key in one request, so replaying it has no partial state
|
||||
to damage.
|
||||
|
||||
A download is retried as a whole — request, stream consumption, and decryption —
|
||||
because a socket reset after the response headers have arrived surfaces in the
|
||||
|
||||
Reference in New Issue
Block a user