buildRetryingClient function
Wraps inner so transient network failures do not surface as errors.
Uses RetryClient from package:http, which already implements the
exponential backoff, rather than hand-rolling a loop.
Two rules decide what may be sent twice:
Only safe requests, meaning GET and HEAD. A replayed POST
registers the user twice or creates a second document, so writes are never
retried - one clear failure beats a duplicate record. This is decided from
the outgoing request, before anything is sent, rather than from the response
- a failure to connect has no response to inspect.
The token refresh is not an exception to that, though it reads like one:
it is a POST only because it carries a token, and it creates nothing. It
was allowed here for exactly that reason, on the argument that a single
dropped packet should not end a session. That argument is wrong, because the
API rotates refresh tokens.
Consider a refresh that reaches the server and is processed, rotating the
token, whose response is then lost - a reset connection, or a 502 from a
proxy sitting in front of an app that already did the work. The replay
presents a token the server has now spent, so it answers 401, and
_refreshAndSettle correctly reads a 401 on a refresh as "this session is
over" and clears it. The user is signed out by a dropped packet, which is
the very outcome the retry was added to prevent - and worse than not
retrying, since without it the caller sees a transport error and the stored
token survives to be tried again.
So: do not add the refresh endpoint back here. If refresh tokens ever stop rotating, that is the thing to change first.
Only transient causes. A connection that failed to open, and the gateway statuses a proxy returns while a server restarts. Not 429: the API's rate limit windows run from ten minutes to an hour, so retrying seconds later cannot succeed and only spends more of the budget - the caller is told to back off instead. Not 4xx: the server understood the request and rejected it, and it will reject it again.
delay is exposed so tests do not have to wait.
Implementation
http.Client buildRetryingClient(
http.Client inner, {
int retries = 3,
Duration Function(int retryCount)? delay,
}) {
return _SafeRequestRetryClient(
inner: inner,
retrying: RetryClient(
inner,
retries: retries,
when: _isRetriableResponse,
whenError: _isRetriableError,
delay: delay ?? _backoff,
),
);
}