Generated client retries and refresh resends fire Hooks.OnRetry
Card: https://app.basecamp.com/2914079/buckets/48521764/card_tables/cards/10258170882
Follow-up filed during #137's review.
Generated retries were invisible to Hooks.OnRetry. The generated client's retry loop, and after #137 its resend after a 401 refresh, fired no hook and left every send marked attempt 1 in the request context, so metrics and traces saw a retried generated operation as a single first attempt while the hand-written doRequestURL path reports its resend as attempt 2.
The generated client gains a RetryHook (WithRetryHook), told about each resend before it is made with a Retry: the request as the editors left it, the number of the attempt about to be made, and either the response the last attempt was answered with (body closed) or the transport error — http.Client.Do's own shape, so the SDK can classify it with CheckResponse and hook consumers see the same *hey.Error the hand-written path hands them. The hook fires only for a resend that happens: a failure past the budget, a 401 nothing can refresh, and a first send that succeeds stay silent, as in doRequestURL and basecamp-sdk's loop. Because the hook runs just before the wait, the context is checked before the select as basecamp-sdk does, so a hook that cancels does not lose a coin flip to an elapsed timer and send once more on a dead context.
initGeneratedClient adapts it onto hooks.OnRetry with the hand-written contract: RequestInfo{Attempt: failed}, the next attempt alongside, and the URL as the account-scoped transport will send it.
Attempts now count across the whole operation, refresh resend included (doAttempts runs a numbered range rather than a count), and each send's context carries its attempt. The generated package owns that context key (ContextWithAttempt/AttemptFromContext), since its loop is one of the two writers; hey's contextWithAttempt/attemptFromContext delegate to it, so the logging transport's OnRequestStart reports the right attempt for both clients.
Tests cover: a 503 retried twice fires OnRetry for attempts 2 and 3 with the failed attempt, the retryable SDK error and the transport's URL, and the transport sees attempts 1–3; a refresh resend fires once as attempt 2 with the auth error; attempts keep counting across a refresh (401, 503, 200 → OnRetry for 2 and 3); and no OnRetry for a first send that succeeds, a 503 with no budget, or a 401 nothing can refresh. The three positives fail with the hook disconnected.
TMPDIR=/tmp/t make check green.
Summary by cubic
Fixes the generated client so retries and refresh resends fire Hooks.OnRetry; they previously fired no hook and left every send marked attempt 1 in the request context.
- The generated client gains a
RetryHookthat runs before each resend with the request, the attempt number about to be sent, and the response or transport error that prompted it. - Failures match the hand-written contract: transport errors reach
OnRetryas the SDK's network error and a refresh's 401 as a retryable authentication error. - Attempts count across the whole operation, refresh resend included, and each send carries its attempt in the request context.
- The hook fires only when a resend happens, reports the request the loop sent rather than
Response.Request, and a hook that cancels stops the resend before the transport is asked.