go 107 lines · 3 tabs

Injecting Auth Headers via a Custom http.RoundTripper Decorator in Go

Shared by codesnips Aug 2026
3 tabs
package authtransport

import (
	"errors"
	"net/http"
)

type TokenSource interface {
	Token() (string, error)
}

type Transport struct {
	Source TokenSource
	Base   http.RoundTripper
}

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
	if t.Source == nil {
		return nil, errors.New("authtransport: nil TokenSource")
	}

	token, err := t.Source.Token()
	if err != nil {
		return nil, err
	}

	// Never mutate the caller's request; clone before setting headers.
	cloned := req.Clone(req.Context())
	cloned.Header.Set("Authorization", "Bearer "+token)

	return t.base().RoundTrip(cloned)
}

func (t *Transport) base() http.RoundTripper {
	if t.Base != nil {
		return t.Base
	}
	return http.DefaultTransport
}
3 files · go Explain with highlit

This snippet shows how to wrap Go's http.RoundTripper to transparently attach authentication to every outgoing request, without touching call sites. The RoundTripper interface is the lowest-level extension point in net/http: it takes a *http.Request and returns a *http.Response. By implementing it, one can compose behavior like logging, retries, or auth as layers around the real transport — the classic decorator pattern applied to HTTP.

In authtransport.go, the Transport struct holds a Source that yields tokens and a Base http.RoundTripper that does the actual network work. The RoundTrip method is the heart of the decorator. A crucial detail is that RoundTrip must not mutate the request it is given — the contract says the incoming *http.Request may be reused or inspected by the caller. The code respects this by using req.Clone(req.Context()) to produce a private copy before calling Header.Set. It then delegates to t.base(), which falls back to http.DefaultTransport when Base is nil, mirroring how the standard library behaves.

The token itself comes from a TokenSource abstraction so the transport does not care whether the credential is static, loaded from disk, or refreshed against an OAuth endpoint. In tokensource.go, CachingSource wraps another source and caches the result until shortly before expiry. A sync.Mutex guards the cached token so concurrent requests do not trigger a stampede of refreshes, and an expiryDelta buffer refreshes early to avoid handing out a token that expires mid-flight. This early-refresh trade-off costs a few unnecessary refreshes but prevents 401s from clock skew and in-flight latency.

In client.go, NewClient assembles an *http.Client whose Transport is the decorator, so ordinary calls like client.Get(url) are authenticated automatically. Because the decorator is just an http.RoundTripper, it stacks cleanly with other transports. Note that returning an error from RoundTrip before the request is sent means the body is never consumed, which is the correct behavior on token-fetch failure. Reach for this pattern whenever auth logic would otherwise be duplicated across many call sites.


Related snips

Share this code

Here's the card — post it anywhere.

Injecting Auth Headers via a Custom http.RoundTripper Decorator in Go — share card
Link copied