R2
Proof of concept for the Go 1.22 range functions and provides a simple and easy-to-use interface for sending HTTP requests with retries.
Install / Use
/learn @miyamo2/R2README
r2 - range over http request
r2 is a proof of concept for the Go 1.22 range functions and provides a simple and easy-to-use interface for sending HTTP requests with retries.
Quick Start
Install
go get github.com/miyamo2/r2
Setup GOEXPERIMENT
[!IMPORTANT]
If your Go project is Go 1.23 or higher, this section is not necessary.
go env -w GOEXPERIMENT=rangefunc
Simple Usage
url := "http://example.com"
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
}
for res, err := range r2.Get(ctx, url, opts...) {
if err != nil {
slog.WarnContext(ctx, "something happened.", slog.Any("error", err))
// Note: Even if continue is used, the iterator could be terminated.
// Likewise, if break is used, the request could be re-executed in the background once more.
continue
}
if res == nil {
slog.WarnContext(ctx, "response is nil")
continue
}
if res.StatusCode != http.StatusOK {
slog.WarnContext(ctx, "unexpected status code.", slog.Int("expect", http.StatusOK), slog.Int("got", res.StatusCode))
continue
}
buf, err := io.ReadAll(res.Body)
if err != nil {
slog.ErrorContext(ctx, "failed to read response body.", slog.Any("error", err))
continue
}
slog.InfoContext(ctx, "response", slog.String("response", string(buf)))
// There is no need to close the response body yourself as auto closing is enabled by default.
}
<details>
<summary>vs 'github.com/avast/retry-go'</summary>
url := "http://example.com"
var buf []byte
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
type ErrTooManyRequests struct{
error
RetryAfter time.Duration
}
opts := []retry.Option{
retry.Attempts(3),
retry.Context(ctx),
// In r2, the delay is calculated with the backoff and jitter by default.
// And, if 429 Too Many Requests are returned, the delay is set based on the Retry-After.
retry.DelayType(
func(n uint, err error, config *Config) time.Duration {
if err != nil {
var errTooManyRequests ErrTooManyRequests
if errors.As(err, &ErrTooManyRequests) {
if ErrTooManyRequests.RetryAfter != 0 {
return ErrTooManyRequests.RetryAfter
}
}
}
return retry.BackOffDelay(n, err, config)
}),
}
// In r2, the timeout period per request can be specified with the `WithPeriod` option.
client := http.Client{
Timeout: time.Second,
}
err := retry.Do(
func() error {
res, err := client.Get(url)
if err != nil {
return err
}
if res == nil {
return fmt.Errorf("response is nil")
}
if res.StatusCode == http.StatusTooManyRequests {
retryAfter := res.Header.Get("Retry-After")
if retryAfter != "" {
retryAfterDuration, err := time.ParseDuration(retryAfter)
if err != nil {
return &ErrTooManyRequests{error: fmt.Errorf("429: too many requests")}
}
return &ErrTooManyRequests{error: fmt.Errorf("429: too many requests"), RetryAfter: retryAfterDuration}
}
return &ErrTooManyRequests{error: fmt.Errorf("429: too many requests")}
}
if res.StatusCode >= http.StatusBadRequest && res.StatusCode < http.StatusInternalServerError {
// In r2, client errors other than TooManyRequests are excluded from retries by default.
return nil
}
if res.StatusCode >= http.StatusInternalServerError {
// In r2, automatically retry if the server error response is returned by default.
return fmt.Errorf("5xx: server error response")
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: expected %d, got %d", http.StatusOK, res.StatusCode)
}
// In r2, the response body is automatically closed by default.
defer res.Body.Close()
buf, err = io.ReadAll(res.Body)
if err != nil {
slog.ErrorContext(ctx, "failed to read response body.", slog.Any("error", err))
return err
}
return nil
},
opts...,
)
if err != nil {
// handle error
}
slog.InfoContext(ctx, "response", slog.String("response", string(buf)))
</details>
Features
| Feature | Description |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Get | Send HTTP Get requests until the termination condition is satisfied. |
| Head | Send HTTP Head requests until the termination condition is satisfied. |
| Post | Send HTTP Post requests until the termination condition is satisfied. |
| Put | Send HTTP Put requests until the termination condition is satisfied. |
| Patch | Send HTTP Patch requests until the termination condition is satisfied. |
| Delete | Send HTTP Delete requests until the termination condition is satisfied. |
| PostForm | Send HTTP Post requests with form until the termination condition is satisfied. |
| Do | Send HTTP requests with the given method until the termination condition is satisfied. |
Get
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
}
for res, err := range r2.Get(ctx, "https://example.com", opts...) {
// do something
}
Head
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
}
for res, err := range r2.Head(ctx, "https://example.com", opts...) {
// do something
}
Post
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
r2.WithContentType(r2.ContentTypeApplicationJson),
}
body := bytes.NewBuffer([]byte(`{"foo": "bar"}`))
for res, err := range r2.Post(ctx, "https://example.com", body, opts...) {
// do something
}
Put
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
r2.WithContentType(r2.ContentTypeApplicationJson),
}
body := bytes.NewBuffer([]byte(`{"foo": "bar"}`))
for res, err := range r2.Put(ctx, "https://example.com", body, opts...) {
// do something
}
Patch
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
r2.WithContentType(r2.ContentTypeApplicationJson),
}
body := bytes.NewBuffer([]byte(`{"foo": "bar"}`))
for res, err := range r2.Patch(ctx, "https://example.com", body, opts...) {
// do something
}
Delete
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
r2.WithContentType(r2.ContentTypeApplicationJson),
}
body := bytes.NewBuffer([]byte(`{"foo": "bar"}`))
for res, err := range r2.Delete(ctx, "https://example.com", body, opts...) {
// do something
}
PostForm
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second),
r2.WithContentType(r2.ContentTypeApplicationJson),
}
form := url.Values{"foo": []string{"bar"}}
for res, err := range r2.Post(ctx, "https://example.com", form, opts...) {
// do something
}
Do
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
opts := []r2.Option{
r2.WithMaxRequestAttempts(3),
r2.WithPeriod(time.Second)
