Documentation
¶
Overview ¶
Package roundtrippers is a collection of high quality http.RoundTripper to augment your http.Client.
Example (GET) ¶
package main
import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"github.com/klauspost/compress/zstd"
"github.com/maruel/roundtrippers"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: Check Accept-Encoding first!
w.Header().Set("Content-Encoding", "zstd")
c, err := zstd.NewWriter(w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = c.Write([]byte("Awesome"))
if err = c.Close(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
defer ts.Close()
// Make all HTTP request in the current program:
// - Retry on 429 and 5xx.
// - Add a X-Request-ID for tracking both client and server side.
// - Accept compressed responses with zstandard and brotli, in addition to gzip.
// - Add logging.
// - Add Authorization Bearer header.
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
const apiKey = "secret-key-that-will-not-appear-in-logs!"
// Retry HTTP 429, 5xx.
http.DefaultClient.Transport = &roundtrippers.Retry{
// Add a unique X-Request-ID HTTP header to every requests.
Transport: &roundtrippers.RequestID{
// Accept brotli and zstd response in addition to gzip.
Transport: &roundtrippers.AcceptCompressed{
// Log requests via slog.
Transport: &roundtrippers.Log{
Logger: logger,
// Authenticate.
Transport: &roundtrippers.Header{
Header: http.Header{"Authorization": []string{"Bearer " + apiKey}},
Transport: http.DefaultTransport,
},
},
},
},
}
// Now any request will be logged, authenticated and compressed.
resp, err := http.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
fmt.Printf("GET: %s\n", string(b))
}
Example (POST) ¶
package main
import (
"compress/gzip"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"github.com/maruel/roundtrippers"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ce := r.Header.Get("Content-Encoding"); ce != "gzip" {
http.Error(w, "sorry, I only read gzip", http.StatusBadRequest)
return
}
gz, err := gzip.NewReader(r.Body)
if err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
b, err := io.ReadAll(gz)
if err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
if err = gz.Close(); err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
if s := string(b); s != "hello" {
http.Error(w, fmt.Sprintf("want \"hello\", got %q", s), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("world"))
}))
defer ts.Close()
// Make all HTTP request in the current program:
// - Retry on 429 and 5xx.
// - Add a X-Request-ID for tracking both client and server side.
// - Compress POST body with gzip.
// - Accept compressed responses with zstandard and brotli, in addition to gzip.
// - Add logging.
// - Add Authorization Bearer header.
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
const apiKey = "secret-key-that-will-not-appear-in-logs!"
// Retry HTTP 429, 5xx.
http.DefaultClient.Transport = &roundtrippers.Retry{
// Add a unique X-Request-ID HTTP header to every requests.
Transport: &roundtrippers.RequestID{
// Compress POST body with gzip
Transport: &roundtrippers.PostCompressed{
Encoding: "gzip",
// Accept brotli and zstd response in addition to gzip.
Transport: &roundtrippers.AcceptCompressed{
// Log requests via slog.
Transport: &roundtrippers.Log{
Logger: logger,
// Authenticate.
Transport: &roundtrippers.Header{
Header: http.Header{"Authorization": []string{"Bearer " + apiKey}},
Transport: http.DefaultTransport,
},
},
},
},
},
}
// Now any request will be logged, authenticated and compressed, including POST request.
resp, err := http.Post(ts.URL, "text/plain", strings.NewReader("hello"))
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
fmt.Printf("POST: %s\n", string(b))
}
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultRetryPolicy = ExponentialBackoff{ MaxTryCount: 3, MaxDuration: 10 * time.Second, Exp: 2, }
DefaultRetryPolicy is a reasonable default policy.
Functions ¶
func Unwrap ¶ added in v0.3.0
func Unwrap(rt http.RoundTripper) http.RoundTripper
Unwrap returns the root underlying transport if the RoundTripper implements Unwrapper. Otherwise, it returns the RoundTripper itself.
Types ¶
type AcceptCompressed ¶
type AcceptCompressed struct {
Transport http.RoundTripper
// contains filtered or unexported fields
}
AcceptCompressed empowers the client to accept zstd, br and gzip compressed responses.
Example (Br) ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/andybalholm/brotli"
"github.com/maruel/roundtrippers"
)
func acceptCompressed(r *http.Request, want string) bool {
for encoding := range strings.SplitSeq(r.Header.Get("Accept-Encoding"), ",") {
if strings.TrimSpace(encoding) == want {
return true
}
}
return false
}
func main() {
// Example on how to hook into the HTTP client roundtripper to enable zstd and brotli.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !acceptCompressed(r, "br") {
http.Error(w, "sorry, I only talk br", http.StatusBadRequest)
return
}
w.Header().Set("Content-Encoding", "br")
c := brotli.NewWriter(w)
_, _ = c.Write([]byte("excellent"))
if err := c.Close(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
defer ts.Close()
t := &roundtrippers.AcceptCompressed{Transport: http.DefaultTransport}
c := http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "excellent"
Example (Gzip) ¶
package main
import (
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/maruel/roundtrippers"
)
func acceptCompressed(r *http.Request, want string) bool {
for encoding := range strings.SplitSeq(r.Header.Get("Accept-Encoding"), ",") {
if strings.TrimSpace(encoding) == want {
return true
}
}
return false
}
func main() {
// Example on how to hook into the HTTP client roundtripper to enable zstd and brotli.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !acceptCompressed(r, "gzip") {
http.Error(w, "sorry, I only talk gzip", http.StatusBadRequest)
return
}
w.Header().Set("Content-Encoding", "gzip")
c := gzip.NewWriter(w)
_, _ = c.Write([]byte("excellent"))
if err := c.Close(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
defer ts.Close()
t := &roundtrippers.AcceptCompressed{Transport: http.DefaultTransport}
c := http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "excellent"
Example (Zstd) ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/klauspost/compress/zstd"
"github.com/maruel/roundtrippers"
)
func acceptCompressed(r *http.Request, want string) bool {
for encoding := range strings.SplitSeq(r.Header.Get("Accept-Encoding"), ",") {
if strings.TrimSpace(encoding) == want {
return true
}
}
return false
}
func main() {
// Example on how to hook into the HTTP client roundtripper to enable zstd and brotli.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !acceptCompressed(r, "zstd") {
http.Error(w, "sorry, I only talk zstd", http.StatusBadRequest)
return
}
w.Header().Set("Content-Encoding", "zstd")
c, err := zstd.NewWriter(w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = c.Write([]byte("excellent"))
if err = c.Close(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
defer ts.Close()
t := &roundtrippers.AcceptCompressed{Transport: http.DefaultTransport}
c := http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "excellent"
func (*AcceptCompressed) Unwrap ¶
func (a *AcceptCompressed) Unwrap() http.RoundTripper
type Capture ¶
type Capture struct {
Transport http.RoundTripper
C chan<- Record
// contains filtered or unexported fields
}
Capture is a http.RoundTripper that records each request.
Example (GET) ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"github.com/maruel/roundtrippers"
)
func main() {
// Example on how to hook into the HTTP client roundtripper to capture each HTTP
// response.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Working"))
}))
defer ts.Close()
ch := make(chan roundtrippers.Record, 1)
t := &roundtrippers.Capture{Transport: http.DefaultTransport, C: ch}
c := &http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
// Print the captured request and response.
fmt.Printf("Actual Response: %q\n", string(b))
record := <-ch
fmt.Printf("Recorded Response: %q\n", record.Response.Body)
}
Output: Actual Response: "Working" Recorded Response: {"Working"}
Example (POST) ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/maruel/roundtrippers"
)
func main() {
// Example on how to hook into the HTTP client roundtripper to capture each HTTP
// request, including the POST body.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
_, _ = w.Write([]byte("Working"))
}))
defer ts.Close()
ch := make(chan roundtrippers.Record, 1)
t := &roundtrippers.Capture{Transport: http.DefaultTransport, C: ch}
c := &http.Client{Transport: t}
resp, err := c.Post(ts.URL, "text/plain", strings.NewReader("What are you doing?"))
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
// Print the captured request and response.
fmt.Printf("Actual Response: %q\n", string(b))
record := <-ch
reqBodyReader, err := record.Request.GetBody()
if err != nil {
log.Fatal(err)
}
reqBody, err := io.ReadAll(reqBodyReader)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recorded Request: %q\n", reqBody)
fmt.Printf("Recorded Response: %q\n", record.Response.Body)
}
Output: Actual Response: "Working" Recorded Request: "What are you doing?" Recorded Response: {"Working"}
func (*Capture) Unwrap ¶
func (c *Capture) Unwrap() http.RoundTripper
type ExponentialBackoff ¶ added in v0.2.0
ExponentialBackoff uses exponential backoff.
type Header ¶
type Header struct {
Transport http.RoundTripper
// Header is the headers to add or remove.
// - A key with no value will remove the key from the HTTP request.
// - A key with one value will reset the value for this key.
// - A key with multiple values will append the values to the preexisting ones, if any.
Header http.Header
// contains filtered or unexported fields
}
Header is a http.RoundTripper that adds a set of headers to each request./
It is useful to set the Authorization bearer token to all requests simultaneously on the client.
Example ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"sort"
"strings"
"github.com/maruel/roundtrippers"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var names []string
for k := range r.Header {
if strings.HasPrefix(k, "Test-") {
names = append(names, k)
}
}
sort.Strings(names)
for _, k := range names {
_, _ = fmt.Fprintf(w, "%s=%s\n", k, strings.Join(r.Header[k], ","))
}
}))
defer ts.Close()
h := http.Header{
// A key with no value removes any pre-existing header with this key.
"Test-Remove": nil,
// A key with a single value will forcibly replace any preexisting value.
"Test-Reset": []string{"value"},
// A key with multiple values will append the values.
"Test-Add": []string{"v1", "v2"},
}
c := http.Client{Transport: &roundtrippers.Header{Transport: http.DefaultTransport, Header: h}}
resp, err := c.Get(ts.URL)
resp.Header.Add("Test-Remove", "will be removed")
resp.Header.Add("Test-Reset", "will be reset")
resp.Header.Add("Test-Add", "will be kept")
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "Test-Add=v1,v2\nTest-Reset=value\n"
func (*Header) Unwrap ¶
func (h *Header) Unwrap() http.RoundTripper
type Log ¶
type Log struct {
Transport http.RoundTripper
Logger *slog.Logger
Level slog.Level
IncludeResponseBody bool
// contains filtered or unexported fields
}
Log is a http.RoundTripper that logs each request and response via slog. It defaults to slog.LevelInfo level unless an error is returned from the roundtripper, then the final log is logged at error level.
Example ¶
package main
import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"github.com/maruel/roundtrippers"
)
func main() {
// Example on how to hook into the HTTP client roundtripper to log each HTTP
// request.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Working"))
}))
defer ts.Close()
logger := slog.New(slog.NewTextHandler(os.Stdout,
&slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// For testing reproducibility, remove the timestamp, url, request id and duration.
if a.Key == "time" || a.Key == "url" || a.Key == "id" || a.Key == "dur" {
return slog.Attr{}
}
return a
},
}))
t := &roundtrippers.RequestID{Transport: &roundtrippers.Log{
Transport: http.DefaultTransport,
Logger: logger,
}}
c := http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: level=INFO msg=http method=GET Content-Encoding="" level=INFO msg=http status=200 Content-Encoding="" Content-Length=7 Content-Type="text/plain; charset=utf-8" level=INFO msg=http size=7 err=<nil> Response: "Working"
Example (With_body) ¶
package main
import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"github.com/maruel/roundtrippers"
)
func main() {
// Example on how to hook into the HTTP client roundtripper to log each HTTP
// request and includes the response body.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Working"))
}))
defer ts.Close()
logger := slog.New(slog.NewTextHandler(os.Stdout,
&slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// For testing reproducibility, remove the timestamp, url, request id and duration.
if a.Key == "time" || a.Key == "url" || a.Key == "id" || a.Key == "dur" {
return slog.Attr{}
}
return a
},
}))
t := &roundtrippers.RequestID{Transport: &roundtrippers.Log{
Transport: http.DefaultTransport,
Logger: logger,
Level: slog.LevelDebug,
IncludeResponseBody: true,
}}
c := http.Client{Transport: t}
resp, err := c.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: level=DEBUG msg=http method=GET Content-Encoding="" level=DEBUG msg=http status=200 Content-Encoding="" Content-Length=7 Content-Type="text/plain; charset=utf-8" level=DEBUG msg=http body=Working err=<nil> Response: "Working"
func (*Log) Unwrap ¶
func (l *Log) Unwrap() http.RoundTripper
type PostCompressed ¶
type PostCompressed struct {
Transport http.RoundTripper
// Encoding determines HTTP POST compression. It must be empty or one of: "zstd", "br" or "zstd".
//
// Warning ⚠: compressing POST content is not supported on most servers.
Encoding string
// Level is the compression level.
// - "br" uses values between 1 and 11. If unset, defaults to 3.
// - "gzip" uses values between 1 and 9. If unset, defaults to 3.
// - "zstd" uses values between 1 and 4. If unset, defaults to 2.
Level int
// contains filtered or unexported fields
}
PostCompressed empowers the client to POST zstd, br and gzip compressed requests.
Example (Gzip) ¶
package main
import (
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/maruel/roundtrippers"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ce := r.Header.Get("Content-Encoding"); ce != "gzip" {
http.Error(w, "sorry, I only read gzip", http.StatusBadRequest)
return
}
gz, err := gzip.NewReader(r.Body)
if err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
b, err := io.ReadAll(gz)
if err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
if err = gz.Close(); err != nil {
http.Error(w, "error: "+err.Error(), http.StatusBadRequest)
return
}
if s := string(b); s != "hello" {
http.Error(w, fmt.Sprintf("want \"hello\", got %q", s), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("world"))
}))
defer ts.Close()
c := http.Client{Transport: &roundtrippers.PostCompressed{Transport: http.DefaultTransport, Encoding: "gzip"}}
resp, err := c.Post(ts.URL, "text/plain", strings.NewReader("hello"))
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
if s := string(b); s != "world" {
log.Fatalf("want \"world\", got %q", s)
}
}
func (*PostCompressed) Unwrap ¶
func (p *PostCompressed) Unwrap() http.RoundTripper
type Record ¶
type Record struct {
// Request is guaranteed to have GetBody set is Body was set. Use this to read the POST's body.
Request *http.Request
Response *http.Response
// Err is the error returned by the http.RoundTripper.Do(), if any.
Err error
// contains filtered or unexported fields
}
Record is a captured HTTP request and response by the Capture http.RoundTripper.
type RequestID ¶
type RequestID struct {
Transport http.RoundTripper
// contains filtered or unexported fields
}
RequestID is a http.RoundTripper that adds a unique X-Request-ID to each request.
It is useful to track requests simultaneously on the client and the server or for logging purposes.
Example ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"github.com/maruel/roundtrippers"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Request-ID") == "" {
_, _ = w.Write([]byte("bad"))
} else {
_, _ = w.Write([]byte("good"))
}
}))
defer ts.Close()
c := http.Client{Transport: &roundtrippers.RequestID{Transport: http.DefaultTransport}}
resp, err := c.Get(ts.URL)
if resp == nil || err != nil {
log.Fatal(resp, err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "good"
func (*RequestID) Unwrap ¶
func (r *RequestID) Unwrap() http.RoundTripper
type Retry ¶ added in v0.2.0
type Retry struct {
Transport http.RoundTripper
// Policy determines if an HTTP request should be retried and after how much time.
//
// If unset, defaults to DefaultRetryPolicy.
Policy RetryPolicy
// TimeAfter can be hooked for unit tests to disable sleeping. It defaults to time.After().
TimeAfter func(d time.Duration) <-chan time.Time
}
Retry retries a request on HTTP 429 or 5xx.
Example ¶
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"time"
"github.com/maruel/roundtrippers"
)
func main() {
count := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if count++; count < 3 {
http.Error(w, "slow down", http.StatusTooManyRequests)
} else {
_, _ = w.Write([]byte("good"))
}
}))
defer ts.Close()
c := http.Client{Transport: &roundtrippers.Retry{
Transport: http.DefaultTransport,
// Optionally set a custom policy instead of roundtrippers.DefaultRetryPolicy.
Policy: &roundtrippers.ExponentialBackoff{
MaxTryCount: 10,
MaxDuration: 60 * time.Second,
Exp: 1.5,
},
// Disable sleeping for unit tests with this trick:
TimeAfter: func(time.Duration) <-chan time.Time {
c := make(chan time.Time, 1)
c <- time.Now()
return c
},
}}
resp, err := c.Get(ts.URL)
if resp == nil || err != nil {
log.Fatal(resp, err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if err = resp.Body.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %q\n", string(b))
}
Output: Response: "good"
func (*Retry) Unwrap ¶ added in v0.2.0
func (r *Retry) Unwrap() http.RoundTripper
Unwrap implements Unwrapper.
type RetryPolicy ¶ added in v0.2.0
type RetryPolicy interface {
ShouldRetry(ctx context.Context, start time.Time, try int, err error, resp *http.Response) bool
Backoff(start time.Time, try int) time.Duration
}
RetryPolicy determines when Retry should retry an HTTP request.
Example ¶
package main
import (
"context"
"net/http"
"slices"
"time"
"github.com/maruel/roundtrippers"
)
func main() {
c := http.Client{Transport: &roundtrippers.Retry{
Transport: http.DefaultTransport,
Policy: &PolicyCodes{
RetryPolicy: &roundtrippers.DefaultRetryPolicy,
Codes: []int{http.StatusPaymentRequired},
},
}}
// Call a web site that returns 402.
_, _ = c.Get("http://example.com")
}
// PolicyCodes is a RetryPolicy that will retry on additional status codes.
type PolicyCodes struct {
roundtrippers.RetryPolicy
Codes []int
}
func (r *PolicyCodes) ShouldRetry(ctx context.Context, start time.Time, try int, err error, resp *http.Response) bool {
if resp != nil && slices.Contains(r.Codes, resp.StatusCode) {
return true
}
return r.RetryPolicy.ShouldRetry(ctx, start, try, err, resp)
}
type Throttle ¶ added in v0.4.0
type Throttle struct {
Transport http.RoundTripper
QPS float64
// TimeAfter can be hooked for unit tests to disable sleeping. It defaults to time.After().
TimeAfter func(d time.Duration) <-chan time.Time
// contains filtered or unexported fields
}
Throttle implements a minimalistic time based algorithm to smooth out HTTP requests at exactly QPS or less.
This is meant for use as a client to make sure the access is strictly limited to never trigger a rate limiter on the server. As such, it doesn't have allowance for bursty requests; this is intentionally not a rate limiter.
func (*Throttle) Unwrap ¶ added in v0.4.0
func (t *Throttle) Unwrap() http.RoundTripper
type Unwrapper ¶
type Unwrapper interface {
Unwrap() http.RoundTripper
}
Unwrapper enables users to get the underlying transport when wrapped with a middleware.