Steve Coffman
Posted on July 22, 2020
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.
Posted on July 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.