Go by Example: http.RoundTripper

docs: https://pkg.go.dev/net/http#RoundTripper

ref: Go (Golang) http RoundTripper Explained

use cases:
1) logging
2) retrying
3) auth
4) caching
5) headers manipulation
6) testing

package main
import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "time"
)
const OK_URL = "http://httpbin.org/status/200"
const BAD_REQ_URL = "http://httpbin.org/status/500"
const NEED_AUTH_URL = "http://httpbin.org/basic-auth/bob/pwd"

create a chain of roundtripper before the default one (http.DefaultTransport)

func main() {
    delay := time.Duration(1 * time.Second)
    c := &http.Client{
        Transport: &authRoundTripper{
            next: &retryRoundTripper{
                next: &loggingRoundTripper{
                    next:   http.DefaultTransport,
                    logger: os.Stdout,
                },
                maxRetries: 3,
                delay:      delay,
            },
            user: "bob",
            pwd:  "pwd",
        },
    }

request auth url

    req, err := http.NewRequest(http.MethodGet, NEED_AUTH_URL, nil)
    if err != nil {
        panic(err)
    }
    res, err := c.Do(req)
    if err != nil {
        panic(err)
    }
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
    fmt.Println("\n--- RESPONSE ---")
    fmt.Println("STATUS CODE: ", res.StatusCode)
    fmt.Println("BODY: ", string(body))

request status 500 url (should retry)

    req, err = http.NewRequest(http.MethodGet, "http://httpbin.org/status/500", nil)
    if err != nil {
        panic(err)
    }
    res, err = c.Do(req)
    if err != nil {
        panic(err)
    }
    body, err = ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
    fmt.Println("\n--- RESPONSE ---")
    fmt.Println("STATUS CODE: ", res.StatusCode)
    fmt.Println("BODY: ", string(body))
}
type authRoundTripper struct {
    next      http.RoundTripper
    user, pwd string
}
func (a authRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    r.SetBasicAuth(a.user, a.pwd)
    return a.next.RoundTrip(r)
}
type retryRoundTripper struct {
    next       http.RoundTripper
    maxRetries int
    delay      time.Duration // delay between each retry
}
func (rr retryRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    var attempts int
    for {
        res, err := rr.next.RoundTrip(r)
        attempts++

max retries exceeded

        if attempts == rr.maxRetries {
            return res, err
        }

good outcome

        if err == nil && res.StatusCode < http.StatusInternalServerError {
            return res, err
        }

delay and retry

        select {

return if context is already canceled

        case <-r.Context().Done():
            return res, r.Context().Err()
        case <-time.After(rr.delay):
        }
    }
}
type loggingRoundTripper struct {
    next   http.RoundTripper
    logger io.Writer
}

RoundTrip is a decorator on top of the default roundtripper

func (l loggingRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {

here we can log our message and info

    fmt.Fprintf(l.logger, "[%s] %s %s\n", time.Now().Format(time.ANSIC),
        r.Method, r.URL.String())
    return l.next.RoundTrip(r)
}
$ go run http-roundtripper.go
[Sun Jan 22 16:31:10 2023] GET http://httpbin.org/basic-auth/bob/pwd
--- RESPONSE ---
STATUS CODE:  200
BODY:  {
  "authenticated": true, 
  "user": "bob"
}
[Sun Jan 22 16:31:11 2023] GET http://httpbin.org/status/500
[Sun Jan 22 16:31:12 2023] GET http://httpbin.org/status/500
[Sun Jan 22 16:31:14 2023] GET http://httpbin.org/status/500
--- RESPONSE ---
STATUS CODE:  500
BODY:

Next example: http/httptrace.