feat(httpclient)!: promote to v1.0.0 — retry-on-429 with Retry-After, DoJSONRequest, bump deps

Extend retry loop to handle HTTP 429 Too Many Requests: when the server includes a
Retry-After header, that duration is used as the retry delay; otherwise falls back to
the configured BackOffDelay. Add DoJSONRequest[Req, Resp] free function that serialises
the request body as JSON, sets Content-Type, and delegates response decoding to DoJSON.
Bump logz and xerrors from v0.9.0 to v1.0.0. API committed as stable.
This commit is contained in:
2026-05-11 19:50:16 -06:00
parent 6026ab8a5e
commit 962b0ccf17
6 changed files with 148 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/http"
"strconv"
"time"
retry "github.com/avast/retry-go/v4"
@@ -91,6 +92,7 @@ func (c *httpClient) Do(req *http.Request) (*http.Response, error) {
result, err := c.cb.Execute(func() (any, error) {
var innerErr error
var retryAfterDelay time.Duration
retryErr := retry.Do(
func() error {
if id := logz.GetRequestID(req.Context()); id != "" {
@@ -110,6 +112,14 @@ func (c *httpClient) Do(req *http.Request) (*http.Response, error) {
"status", resp.StatusCode,
"latency", latency.String(),
)
if resp.StatusCode == http.StatusTooManyRequests {
if ra := resp.Header.Get("Retry-After"); ra != "" {
if secs, err := strconv.Atoi(ra); err == nil {
retryAfterDelay = time.Duration(secs) * time.Second
}
}
return fmt.Errorf("rate limited: %d", resp.StatusCode)
}
if resp.StatusCode >= 500 {
return fmt.Errorf("server error: %d", resp.StatusCode)
}
@@ -117,7 +127,14 @@ func (c *httpClient) Do(req *http.Request) (*http.Response, error) {
},
retry.Attempts(c.cfg.MaxRetries),
retry.Delay(c.cfg.RetryDelay),
retry.DelayType(retry.BackOffDelay),
retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
if retryAfterDelay > 0 {
d := retryAfterDelay
retryAfterDelay = 0
return d
}
return retry.BackOffDelay(n, err, config)
}),
retry.LastErrorOnly(true),
)
return resp, retryErr