Skip to content

Commit eb08cce

Browse files
committed
Merge branch 'main' into noretry-header-cert
2 parents a1a8ab8 + 4fb315e commit eb08cce

14 files changed

+370
-91
lines changed

.github/workflows/actionlint.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
actionlint:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
15+
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
1616
- name: "Check GitHub workflow files"
1717
uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest
1818
with:

.github/workflows/go-retryablehttp.yml

-46
This file was deleted.

.github/workflows/pr-gofmt.yaml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Go format check
2+
on:
3+
pull_request:
4+
types: ['opened', 'synchronize']
5+
6+
jobs:
7+
run-tests:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
11+
12+
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
13+
with:
14+
go-version-file: ./.go-version
15+
16+
- name: Run go format
17+
run: |-
18+
files=$(gofmt -s -l .)
19+
if [ -n "$files" ]; then
20+
echo >&2 "The following file(s) are not gofmt compliant:"
21+
echo >&2 "$files"
22+
exit 1
23+
fi
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Unit tests (Go 1.19)
2+
on:
3+
pull_request:
4+
types: ['opened', 'synchronize']
5+
6+
jobs:
7+
run-tests:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
11+
12+
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
13+
with:
14+
go-version: 1.19
15+
16+
- name: Run unit tests
17+
run: make test
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Unit tests (Go 1.20+)
2+
on:
3+
pull_request:
4+
types: ['opened', 'synchronize']
5+
6+
jobs:
7+
run-tests:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
11+
12+
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
13+
with:
14+
go-version: 1.22
15+
16+
- name: Run unit tests
17+
run: make test

.go-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.22.2

CHANGELOG.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1+
## 0.7.6 (Unreleased)
2+
3+
ENHANCEMENTS:
4+
5+
- client: support a `RetryPrepare` function for modifying the request before retrying (#216)
6+
- client: support HTTP-date values for `Retry-After` header value (#138)
7+
- client: avoid reading entire body when the body is a `*bytes.Reader` (#197)
8+
19
## 0.7.5 (Nov 8, 2023)
210

3-
BUG FIXES
11+
BUG FIXES:
412

5-
- client: fixes an issue where the request body is not preserved on temporary redirects or re-established HTTP/2 connections [GH-207]
13+
- client: fixes an issue where the request body is not preserved on temporary redirects or re-established HTTP/2 connections (#207)
614

715
## 0.7.4 (Jun 6, 2023)
816

9-
BUG FIXES
17+
BUG FIXES:
1018

11-
- client: fixing an issue where the Content-Type header wouldn't be sent with an empty payload when using HTTP/2 [GH-194]
19+
- client: fixing an issue where the Content-Type header wouldn't be sent with an empty payload when using HTTP/2 (#194)
1220

1321
## 0.7.3 (May 15, 2023)
1422

CODEOWNERS

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* @hashicorp/release-engineering
1+
* @hashicorp/go-retryablehttp-maintainers

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ default: test
22

33
test:
44
go vet ./...
5-
go test -race ./...
5+
go test -v -race ./...
66

77
updatedeps:
88
go get -f -t -u ./...

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,4 @@ standardClient := retryClient.StandardClient() // *http.Client
5959
```
6060

6161
For more usage and examples see the
62-
[godoc](http://godoc.org/github.com/hashicorp/go-retryablehttp).
62+
[pkg.go.dev](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp).

client.go

+70-21
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"context"
3030
"fmt"
3131
"io"
32-
"io/ioutil"
3332
"log"
3433
"math"
3534
"math/rand"
@@ -62,6 +61,10 @@ var (
6261
// limit the size we consume to respReadLimit.
6362
respReadLimit = int64(4096)
6463

64+
// timeNow sets the function that returns the current time.
65+
// This defaults to time.Now. Changes to this should only be done in tests.
66+
timeNow = time.Now
67+
6568
// A regular expression to match the error returned by net/http when the
6669
// configured number of redirects is exhausted. This error isn't typed
6770
// specifically so we resort to matching on the error string.
@@ -252,29 +255,27 @@ func getBodyReaderAndContentLength(rawBody interface{}) (ReaderFunc, int64, erro
252255
// deal with it seeking so want it to match here instead of the
253256
// io.ReadSeeker case.
254257
case *bytes.Reader:
255-
buf, err := ioutil.ReadAll(body)
256-
if err != nil {
257-
return nil, 0, err
258-
}
258+
snapshot := *body
259259
bodyReader = func() (io.Reader, error) {
260-
return bytes.NewReader(buf), nil
260+
r := snapshot
261+
return &r, nil
261262
}
262-
contentLength = int64(len(buf))
263+
contentLength = int64(body.Len())
263264

264265
// Compat case
265266
case io.ReadSeeker:
266267
raw := body
267268
bodyReader = func() (io.Reader, error) {
268269
_, err := raw.Seek(0, 0)
269-
return ioutil.NopCloser(raw), err
270+
return io.NopCloser(raw), err
270271
}
271272
if lr, ok := raw.(LenReader); ok {
272273
contentLength = int64(lr.Len())
273274
}
274275

275276
// Read all in so we can reset
276277
case io.Reader:
277-
buf, err := ioutil.ReadAll(body)
278+
buf, err := io.ReadAll(body)
278279
if err != nil {
279280
return nil, 0, err
280281
}
@@ -397,6 +398,9 @@ type Backoff func(min, max time.Duration, attemptNum int, resp *http.Response) t
397398
// attempted. If overriding this, be sure to close the body if needed.
398399
type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Response, error)
399400

401+
// PrepareRetry is called before retry operation. It can be used for example to re-sign the request
402+
type PrepareRetry func(req *http.Request) error
403+
400404
// Client is used to make HTTP requests. It adds additional functionality
401405
// like automatic retries to tolerate minor outages.
402406
type Client struct {
@@ -425,6 +429,9 @@ type Client struct {
425429
// ErrorHandler specifies the custom error handler to use, if any
426430
ErrorHandler ErrorHandler
427431

432+
// PrepareRetry can prepare the request for retry operation, for example re-sign it
433+
PrepareRetry PrepareRetry
434+
428435
loggerInit sync.Once
429436
clientInit sync.Once
430437
}
@@ -544,10 +551,8 @@ func baseRetryPolicy(resp *http.Response, err error) (bool, error) {
544551
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
545552
if resp != nil {
546553
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
547-
if s, ok := resp.Header["Retry-After"]; ok {
548-
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
549-
return time.Second * time.Duration(sleep)
550-
}
554+
if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
555+
return sleep
551556
}
552557
}
553558
}
@@ -560,6 +565,41 @@ func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response)
560565
return sleep
561566
}
562567

568+
// parseRetryAfterHeader parses the Retry-After header and returns the
569+
// delay duration according to the spec: https://httpwg.org/specs/rfc7231.html#header.retry-after
570+
// The bool returned will be true if the header was successfully parsed.
571+
// Otherwise, the header was either not present, or was not parseable according to the spec.
572+
//
573+
// Retry-After headers come in two flavors: Seconds or HTTP-Date
574+
//
575+
// Examples:
576+
// * Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
577+
// * Retry-After: 120
578+
func parseRetryAfterHeader(headers []string) (time.Duration, bool) {
579+
if len(headers) == 0 || headers[0] == "" {
580+
return 0, false
581+
}
582+
header := headers[0]
583+
// Retry-After: 120
584+
if sleep, err := strconv.ParseInt(header, 10, 64); err == nil {
585+
if sleep < 0 { // a negative sleep doesn't make sense
586+
return 0, false
587+
}
588+
return time.Second * time.Duration(sleep), true
589+
}
590+
591+
// Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
592+
retryTime, err := time.Parse(time.RFC1123, header)
593+
if err != nil {
594+
return 0, false
595+
}
596+
if until := retryTime.Sub(timeNow()); until > 0 {
597+
return until, true
598+
}
599+
// date is in the past
600+
return 0, true
601+
}
602+
563603
// LinearJitterBackoff provides a callback for Client.Backoff which will
564604
// perform linear backoff based on the attempt number and with jitter to
565605
// prevent a thundering herd.
@@ -587,13 +627,13 @@ func LinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Resp
587627
}
588628

589629
// Seed rand; doing this every time is fine
590-
rand := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
630+
source := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
591631

592632
// Pick a random number that lies somewhere between the min and max and
593633
// multiply by the attemptNum. attemptNum starts at zero so we always
594634
// increment here. We first get a random percentage, then apply that to the
595635
// difference between min and max, and add to min.
596-
jitter := rand.Float64() * float64(max-min)
636+
jitter := source.Float64() * float64(max-min)
597637
jitterMin := int64(jitter) + int64(min)
598638
return time.Duration(jitterMin * int64(attemptNum))
599639
}
@@ -627,10 +667,10 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
627667
var resp *http.Response
628668
var attempt int
629669
var shouldRetry bool
630-
var doErr, respErr, checkErr error
670+
var doErr, respErr, checkErr, prepareErr error
631671

632672
for i := 0; ; i++ {
633-
doErr, respErr = nil, nil
673+
doErr, respErr, prepareErr = nil, nil, nil
634674
attempt++
635675

636676
// Always rewind the request body when non-nil.
@@ -643,7 +683,7 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
643683
if c, ok := body.(io.ReadCloser); ok {
644684
req.Body = c
645685
} else {
646-
req.Body = ioutil.NopCloser(body)
686+
req.Body = io.NopCloser(body)
647687
}
648688
}
649689

@@ -737,17 +777,26 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
737777
// without racing against the closeBody call in persistConn.writeLoop.
738778
httpreq := *req.Request
739779
req.Request = &httpreq
780+
781+
if c.PrepareRetry != nil {
782+
if err := c.PrepareRetry(req.Request); err != nil {
783+
prepareErr = err
784+
break
785+
}
786+
}
740787
}
741788

742789
// this is the closest we have to success criteria
743-
if doErr == nil && respErr == nil && checkErr == nil && !shouldRetry {
790+
if doErr == nil && respErr == nil && checkErr == nil && prepareErr == nil && !shouldRetry {
744791
return resp, nil
745792
}
746793

747794
defer c.HTTPClient.CloseIdleConnections()
748795

749796
var err error
750-
if checkErr != nil {
797+
if prepareErr != nil {
798+
err = prepareErr
799+
} else if checkErr != nil {
751800
err = checkErr
752801
} else if respErr != nil {
753802
err = respErr
@@ -779,7 +828,7 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
779828
// Try to read the response body so we can reuse this connection.
780829
func (c *Client) drainBody(body io.ReadCloser) {
781830
defer body.Close()
782-
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
831+
_, err := io.Copy(io.Discard, io.LimitReader(body, respReadLimit))
783832
if err != nil {
784833
if c.logger() != nil {
785834
switch v := c.logger().(type) {

0 commit comments

Comments
 (0)