sendWatchedProviderRequest function
Future<StreamedResponse>
sendWatchedProviderRequest(
- Client httpClient,
- Request request,
- CancelToken? cancelToken
One watched send: the cancel-token race plus the connect watchdog, with a bounded TRANSPARENT retry when the watchdog kills a request that never received a byte (issue #1121).
Public for transports that must own their response semantics and so bypass sendProviderRequest's redirect decision and status/body validation — chatgpt-codex stores Cloudflare cookies from error responses and replays challenges itself, so it takes the raw watched send and keeps its own cookie/challenge logic.
Implementation
Future<http.StreamedResponse> sendWatchedProviderRequest(
http.Client httpClient,
http.Request request,
CancelToken? cancelToken,
) async {
for (var attempt = 0; ; attempt++) {
// A sent `http.Request` is finalized by the client and cannot be sent
// again — every attempt after the first rides a fresh clone (the
// redirect `_reissue` clones for the same reason).
final attemptRequest = attempt == 0
? request
: _reissue(request, request.url, 0);
try {
return await _sendWatchedOnce(
httpClient,
attemptRequest,
cancelToken,
attempt,
);
} on TimeoutException {
// The connect watchdog fired (issue #1121). `httpClient.send`
// completes only when the response HEADERS arrive, so a timeout in
// THIS layer means zero response bytes — the provider never started
// generating. That makes an in-place retry safe where a mid-stream
// retry is not: no partial stream to corrupt, no committed state, and
// nothing double-billed (a request the endpoint never answered is not
// a billed generation) — the identical payload is simply re-sent.
// The budget is deliberately separate from the roles failover ladder
// (a network stall must not rotate the model chain, #1066 semantics)
// and from TransientRetryStream (governed by bytes AFTER the first).
// Mid-stream silence never re-enters here — the idle watchdog fires
// downstream in createSseIterator — so only the never-started request
// ever retries.
if (attempt >= providerConnectRetries) rethrow;
final delay = providerConnectRetryBackoff * (1 << attempt);
// Observable on the EXISTING retry surface: the CLI host prints a dim
// `[net]` line + fa.log entry; hosts leaving the hook null stay
// silent — no new telemetry.
transientRetryNotice?.call(
attempt + 1,
providerConnectRetries + 1,
delay,
'connect stall: no response bytes',
);
// Bounded backoff; the sleeper races the cancel token, so a user
// abort during the wait wins over the pending retry.
final survived = await transientRetrySleeper(delay, cancelToken);
if (!survived) {
cancelToken?.throwIfCancelled(); // the abort propagates
rethrow;
}
}
}
}