Tripperwares: http.Client Middleware - chaining RoundTrippers

stevenacoffman

Steve Coffman

Posted on July 22, 2020

Tripperwares: http.Client Middleware - chaining RoundTrippers

http.Client Middleware or "Tripperwares" adhere to func (http.RoundTripper) http.RoundTripper signature, hence the name. As such they are used as a Transport for http.Client, and can wrap other transports.

This means that the composition is purely net/http-based.

While server middleware is pretty commonly discussed (and there's lots to pick from), I found the documentation a little sparse on how (and why) to do this for clients. Luckily the amazing @PeterBourgon helped me on the #gophers slack.

A couple good use cases for TripperWare are request logging, caching, and certain kinds of auth (e.g. bearer token renewal).

However, instead of making one complicated Tripperware to do all this, it's nice to compose several more tightly focussed ones.

package main

import (
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

// thanks to peter bourgon on Gophers slack

func main() {
    jiraUserID := os.ExpandEnv("${JIRA_USERID}")
    jiraPassword := os.ExpandEnv("${JIRA_API_TOKEN}")
    jiraBaseURL := "https://example.atlassian.net/rest/agile/1.0/board/"

    var rt http.RoundTripper
    rt = http.DefaultTransport
    rt = NewDelayRoundTripper(rt, 123*time.Millisecond)
    rt = NewLoggingRoundTripper(rt, os.Stderr)
    header := make(http.Header)
    header.Set("Accept", "application/json")
    header.Set("Content-Type", "application/json")
    hrt := NewHeaderRoundTripper(rt, header)
    hrt.BasicAuth(jiraUserID, jiraPassword)

    client := &http.Client{
        Transport: hrt,
    }

    _, err := client.Get(jiraBaseURL)
    fmt.Printf("err=%v\n", err)
}

//
//
//

type DelayRoundTripper struct {
    next  http.RoundTripper
    delay time.Duration
}

func NewDelayRoundTripper(next http.RoundTripper, delay time.Duration) *DelayRoundTripper {
    return &DelayRoundTripper{
        next:  next,
        delay: delay,
    }
}

func (rt *DelayRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
    time.Sleep(rt.delay)
    return rt.next.RoundTrip(req)
}

//
//
//

type LoggingRoundTripper struct {
    next http.RoundTripper
    dst  io.Writer
}

func NewLoggingRoundTripper(next http.RoundTripper, dst io.Writer) *LoggingRoundTripper {
    return &LoggingRoundTripper{
        next: next,
        dst:  dst,
    }
}

func (rt *LoggingRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
    defer func(begin time.Time) {
        fmt.Fprintf(rt.dst,
            "method=%s host=%s status_code=%d err=%v took=%s\n",
            req.Method, req.URL.Host, resp.StatusCode, err, time.Since(begin),
        )
    }(time.Now())

    return rt.next.RoundTrip(req)
}

//
//
//

type HeaderRoundTripper struct {
    next http.RoundTripper
    Header http.Header
}

func NewHeaderRoundTripper(next http.RoundTripper, Header http.Header) *HeaderRoundTripper {
    if next == nil {
        next = http.DefaultTransport
    }
    return &HeaderRoundTripper{
        next:   next,
        Header: Header,
    }
}

func (rt *HeaderRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
    if rt.Header != nil {
        for k, v := range rt.Header {
            req.Header[k] = v
        }
    }
    fmt.Println("HeaderRoundTrip")
    return rt.next.RoundTrip(req)
}

func (rt *HeaderRoundTripper) BasicAuth(username, password string) {
    if rt.Header == nil {
        rt.Header = make(http.Header)
    }

    auth := username + ":" + password
    base64Auth := base64.StdEncoding.EncodeToString([]byte(auth))
    rt.Header.Set("Authorization", "Basic "+ base64Auth)
}

func (rt *HeaderRoundTripper) SetHeader(key, value string) {
    if rt.Header == nil {
        rt.Header = make(http.Header)
    }
    rt.Header.Set(key, value)
}

This works fine, but if you're less into receivers and prefer functional composition, here's an functional alternative:

// NewRoundTripper returns an http.RoundTripper that is tooled for use in the app

func NewRoundTripper(original http.RoundTripper) http.RoundTripper {
    if original == nil {
        original = http.DefaultTransport
    }

    return roundTripperFunc(func(request *http.Request) (*http.Response, error) {
        response, err := original.RoundTrip(request)
        return response, err
    })
}

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }

I found that code in evepraisal/go-evepraisal where @sudorandom used it to instrument external calls with New Relic monitoring.

Also, I found that there's a several tripperwares and some helper libraries here: improbable-eng/go-httpwares

It's worth mentioning that changing the request is a bit of a no-no. Cloning the request, modifying that, and then passing that on is a workaround

Here's an article about an alternative for header manipulation.

💖 💪 🙅 🚩
stevenacoffman
Steve Coffman

Posted on July 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related