Skip to content

Commit 47fb7db

Browse files
committed
lots of cleanup, a few test additions
1 parent 1ebb984 commit 47fb7db

4 files changed

Lines changed: 385 additions & 40 deletions

File tree

jttp.go

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,18 @@ const (
3232
DefaultMaxRedirects = 5
3333
DefaultMaxIdleConns = 20
3434
DefaultMaxIdleConnsPerHost = 20
35-
DefaultMaxConnsPerHost = 0 // unlimited
35+
DefaultMaxConnsPerHost = 100
3636
DefaultIdleConnTimeout = 90 * time.Second
3737
DefaultTLSHandshakeTimeout = 5 * time.Second
38-
DefaultResponseHeaderTimeout = 0 // use Client.Timeout instead
38+
DefaultResponseHeaderTimeout = 10 * time.Second
3939
DefaultDialTimeout = 5 * time.Second
4040
DefaultDialKeepAlive = 30 * time.Second
4141
DefaultMaxRetries = 3
4242
DefaultRetryWaitMin = 250 * time.Millisecond
4343
DefaultRetryWaitMax = 2 * time.Second
4444
DefaultExpectContinueTimeout = 2 * time.Second
4545
DefaultMaxRetryBodyBytes = 4 << 20 // 4 MiB
46+
DefaultMaxRetryAfter = 1 * time.Minute
4647
)
4748

4849
type config struct {
@@ -69,6 +70,7 @@ type config struct {
6970
maxRetries int
7071
retryWaitMin time.Duration
7172
retryWaitMax time.Duration
73+
maxRetryAfter time.Duration
7274
maxRetryBodyBytes int64
7375
retryableStatusCodes map[int]struct{}
7476
retryableMethods map[string]struct{}
@@ -99,6 +101,7 @@ func defaults() *config {
99101
maxRetries: DefaultMaxRetries,
100102
retryWaitMin: DefaultRetryWaitMin,
101103
retryWaitMax: DefaultRetryWaitMax,
104+
maxRetryAfter: DefaultMaxRetryAfter,
102105
maxRetryBodyBytes: DefaultMaxRetryBodyBytes,
103106
retryableStatusCodes: map[int]struct{}{
104107
http.StatusTooManyRequests: {},
@@ -119,18 +122,16 @@ type Option func(*config)
119122

120123
// New creates a new *http.Client with good defaults.
121124
// All defaults can be overridden via Option values.
125+
// As with all http.Clients, be sure to use the returned
126+
// client across the lifetime of multiple requests.
122127
func New(opts ...Option) *http.Client {
123128
cfg := defaults()
124129
for _, opt := range opts {
125130
opt(cfg)
126131
}
127132

128-
if cfg.maxRetries < 0 {
129-
cfg.maxRetries = 0
130-
}
131-
if cfg.maxRedirects < 0 {
132-
cfg.maxRedirects = 0
133-
}
133+
cfg.maxRetries = max(cfg.maxRetries, 0)
134+
cfg.maxRedirects = max(cfg.maxRedirects, 0)
134135
if cfg.retryWaitMin <= 0 {
135136
cfg.retryWaitMin = DefaultRetryWaitMin
136137
}
@@ -140,6 +141,19 @@ func New(opts ...Option) *http.Client {
140141
if cfg.retryWaitMin > cfg.retryWaitMax {
141142
cfg.retryWaitMin, cfg.retryWaitMax = cfg.retryWaitMax, cfg.retryWaitMin
142143
}
144+
if cfg.maxRetryAfter <= 0 {
145+
cfg.maxRetryAfter = DefaultMaxRetryAfter
146+
}
147+
148+
// Clamp negative durations for transport-level settings.
149+
// Zero is valid and means "no limit" for most of these.
150+
cfg.timeout = max(cfg.timeout, 0)
151+
cfg.dialTimeout = max(cfg.dialTimeout, 0)
152+
cfg.dialKeepAlive = max(cfg.dialKeepAlive, 0)
153+
cfg.idleConnTimeout = max(cfg.idleConnTimeout, 0)
154+
cfg.tlsHandshakeTimeout = max(cfg.tlsHandshakeTimeout, 0)
155+
cfg.responseHeaderTimeout = max(cfg.responseHeaderTimeout, 0)
156+
cfg.expectContinueTimeout = max(cfg.expectContinueTimeout, 0)
143157

144158
var base http.RoundTripper
145159
if cfg.transport != nil {
@@ -148,6 +162,8 @@ func New(opts ...Option) *http.Client {
148162
tlsCfg := cfg.tlsConfig
149163
if tlsCfg == nil {
150164
tlsCfg = &tls.Config{}
165+
} else {
166+
tlsCfg = tlsCfg.Clone()
151167
}
152168
if tlsCfg.MinVersion < tls.VersionTLS12 {
153169
tlsCfg.MinVersion = tls.VersionTLS12
@@ -177,6 +193,7 @@ func New(opts ...Option) *http.Client {
177193
maxRetries: cfg.maxRetries,
178194
waitMin: cfg.retryWaitMin,
179195
waitMax: cfg.retryWaitMax,
196+
maxRetryAfter: cfg.maxRetryAfter,
180197
maxRetryBodyBytes: cfg.maxRetryBodyBytes,
181198
retryableCodes: cfg.retryableStatusCodes,
182199
retryableMethods: cfg.retryableMethods,
@@ -243,7 +260,7 @@ func WithMaxIdleConnsPerHost(n int) Option {
243260
}
244261

245262
// WithMaxConnsPerHost sets the maximum total connections per host.
246-
// 0 means unlimited (the default).
263+
// 0 means unlimited. Default: 100.
247264
func WithMaxConnsPerHost(n int) Option {
248265
return func(c *config) { c.maxConnsPerHost = n }
249266
}
@@ -261,7 +278,7 @@ func WithTLSHandshakeTimeout(d time.Duration) Option {
261278
}
262279

263280
// WithResponseHeaderTimeout sets the maximum time to wait for response headers
264-
// after the request is fully written. 0 means no limit (the default).
281+
// after the request is fully written. 0 means no limit. Default: 10s.
265282
func WithResponseHeaderTimeout(d time.Duration) Option {
266283
return func(c *config) { c.responseHeaderTimeout = d }
267284
}
@@ -278,9 +295,14 @@ func WithRetries(n int) Option {
278295
return func(c *config) { c.maxRetries = n }
279296
}
280297

298+
// WithNoRetries disables retry logic entirely.
299+
func WithNoRetries() Option {
300+
return WithRetries(0)
301+
}
302+
281303
// WithRetryWait sets the minimum and maximum wait times between retries.
282304
// Backoff is exponential with full jitter within these bounds.
283-
// Default: 1s min, 10s max.
305+
// Default: 250ms min, 2s max.
284306
func WithRetryWait(minWait, maxWait time.Duration) Option {
285307
return func(c *config) {
286308
c.retryWaitMin = minWait
@@ -334,6 +356,14 @@ func WithAdditionalRetryableMethods(methods ...string) Option {
334356
}
335357
}
336358

359+
// WithMaxRetryAfter sets the maximum duration that a server's Retry-After
360+
// header will be respected. If the server requests a longer delay, it will
361+
// be capped at this value. Retry-After values are also floored at the
362+
// minimum retry wait time (see WithRetryWait). Default: 1 minute.
363+
func WithMaxRetryAfter(d time.Duration) Option {
364+
return func(c *config) { c.maxRetryAfter = d }
365+
}
366+
337367
// WithMaxRetryBodyBytes sets the maximum request body size (in bytes) that will
338368
// be buffered into memory for retry support. Bodies larger than this limit cause
339369
// an error when retries are enabled and the body is not already seekable.
@@ -350,9 +380,11 @@ func WithCheckRetry(fn func(req *http.Request, resp *http.Response, err error) b
350380
return func(c *config) { c.checkRetry = fn }
351381
}
352382

353-
// WithRetryObserver registers a callback that is invoked each time a retry
354-
// decision is made. The attempt number is 0-indexed (0 = first attempt that
355-
// will be retried). This is useful for logging or metrics.
383+
// WithRetryObserver registers a callback that is invoked before each retry
384+
// attempt. The attempt number is 0-indexed (0 = first failed attempt that
385+
// will be retried). This is not called on the final exhausted attempt —
386+
// only when a retry will actually follow. This is useful for logging or
387+
// metrics.
356388
func WithRetryObserver(fn func(attempt int, req *http.Request, resp *http.Response, err error)) Option {
357389
return func(c *config) { c.retryObserver = fn }
358390
}

0 commit comments

Comments
 (0)