SkillAgentSearch skills...

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/R2
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

r2 - range over http request

Go Reference GitHub go.mod Go version GitHub release (latest by date) codecov Go Report Card GitHub License

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)
View on GitHub
GitHub Stars6
CategoryDevelopment
Updated1y ago
Forks0

Languages

Go

Security Score

75/100

Audited on Oct 6, 2024

No findings