transport – zgo.at/transport Index | Examples | Files

package transport

import "zgo.at/transport"

Package transport contains HTTP transports for http.Client.

All of these implement http.RoundTripper so it can be used with any http.Client and won't require any changes other than setting the http.Client.Transport field.

Every transport accepts a parent transport; multiple transports can be used by calling several of them. For example:

c := http.Client{
	Transport: transport.Retry(transport.Cache(http.DefaultTransport)),
}

This is run from the outer-most call to the inner-most (Retry → Cache → DefaultTransport).

Index

Examples

Constants

const (
	LogAll            = LogRequestHeaders | LogRequestBody | LogResponseHeaders | LogResponseBody
	LogRequestHeaders = LogOption(1 << iota)
	LogRequestBody
	LogResponseHeaders
	LogResponseBody
)

Variables

var ErrFiltered = errors.New("transport.Filter: request not allowed")

Functions

func Cache

func Cache(parent http.RoundTripper, s CacheStorer, e CacheExpirer) *cache

Cache requests in the given storer.

The expiry is determined by the expirer, which may be nil to cache forever. Expired resourced are not cleaned: cached resources can only be overwritten with a newer version.

Example

Code:play 

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"time"

	"zgo.at/transport"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("=> Handle")
		w.Write([]byte("Response body"))
	}))
	defer srv.Close()

	c := http.Client{
		Transport: transport.Cache(
			http.DefaultTransport,
			transport.CacheMemory(),              // Cache in memory.
			transport.CacheExpireTime(time.Hour), // Expire after an hour.
		),
	}

	read := func(resp *http.Response) string {
		defer resp.Body.Close()
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			log.Fatal(err)
		}
		return string(b)
	}
	resp, err := c.Get(srv.URL)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(read(resp))

	// Second request: same response body but handler not called.
	resp, err = c.Get(srv.URL)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(read(resp))

}

Output:

=> Handle
Response body
Response body

func CacheExpireTime

func CacheExpireTime(d time.Duration) *expireTime

CacheExpireTime expires resources if they're older than d.

This does not look at any HTTP cache headers; it simply cached for the duration of d.

func CacheFile

func CacheFile(path string) *cacheFile

CacheFile caches resources on disk.

func CacheMemory

func CacheMemory() *cacheMemory

CacheMemory caches request in memory.

Because expired resourced are not cleaned this may use a large amount of memory over time. This implementation is intentionally kept simple – use a more advance memory cache such as e.g. https://zgo.at/zcache if you need to clean expired resources.

func CacheNop

func CacheNop() *cacheNop

CacheNop returns a storer that doesn't do anything.

func Filter

func Filter(parent http.RoundTripper, allow func(*url.URL) (bool, error)) *filter

Filter requests.

Requests return ErrFiltered if allow returns false. It can optionally return an error with some additional information.

Example

Code:play 

package main

import (
	"fmt"
	"net/http"

	"zgo.at/transport"
)

func main() {
	c := http.Client{
		// Disallow all requests to local/private addresses such as localhost, 10/8, etc.
		Transport: transport.Filter(http.DefaultTransport, transport.FilterLocal),
	}
	resp, err := c.Get("http://localhost/test")
	fmt.Println(err)
	fmt.Println(resp == nil)

}

Output:

Get "http://localhost/test": transport.Filter: request not allowed: FilterLocal: "localhost" is not allowed
true

func FilterLocal

func FilterLocal(u *url.URL) (bool, error)

FilterLocal filters all requests to local addresses.

func HTTPError

func HTTPError(only500 bool, bodyLimit int) func(resp *http.Response, err error) (*http.Response, error)

HTTPError returns an Intercept function to return errors if the HTTP status code is >=400 or >=500.

This simplifies error handling for some common use cases.

It only handles 5xx if only500 is set. Otherwise it will handle both 4xx and 5xx errors.

It returns ErrHTTPError, which contains the first bodyLimit bytes of the response body.

func Intercept

func Intercept(parent http.RoundTripper, fn func(*http.Response, error) (*http.Response, error)) *intercept

Intercept responses and change the response or error.

One returned parameter must be nil: it's not allowed to return both a response and an error.

Example

Code:play 

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"regexp"

	"zgo.at/transport"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(500)
		w.Write([]byte("error text from HTTP handler"))
	}))
	defer srv.Close()

	c := http.Client{
		// Return an error on all status codes >=400.
		Transport: transport.Intercept(http.DefaultTransport, transport.HTTPError(false, 1024)),
	}
	_, err := c.Get(srv.URL)

	// Replace some dynamic text with static text for test.
	fmt.Println(regexp.MustCompile(`(Get "http://.+?):\d+"`).ReplaceAllString(err.Error(), `$1:80"`))

}

Output:

Get "http://127.0.0.1:80": HTTP status 500: error text from HTTP handler

func Log

func Log(parent http.RoundTripper, out io.Writer, what LogOption) *log

Log writes request and/or response details to out.

Example

Code:play 

package main

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strings"

	"zgo.at/transport"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Handle"))
	}))
	defer srv.Close()

	buf := new(bytes.Buffer)
	c := http.Client{
		// Log request and response headers and body.
		Transport: transport.Log(http.DefaultTransport, buf, transport.LogAll),
	}
	resp, err := c.Post(srv.URL, "application/json", strings.NewReader(`[1, 2, 3]`))
	if err != nil {
		log.Fatal(err)
	}
	resp.Body.Close()

	// Replace some dynamic text with static text for test.
	have := buf.String()
	have = regexp.MustCompile(`(Host: *127\.0\.0\.1:)\d+`).ReplaceAllString(have, `$1`)
	have = regexp.MustCompile(`(Date: *).+?\n`).ReplaceAllString(have, "${1}Tue, 21 Apr 2026 21:13:48 GMT\n")
	fmt.Print(have)

}

Output:

REQ │ POST / HTTP/1.1
REQ │ Host:            127.0.0.1:
REQ │ Accept-Encoding: gzip
REQ │ Content-Length:  9
REQ │ Content-Type:    application/json
REQ │ User-Agent:      Go-http-client/1.1
REQ │
REQ │ [1, 2, 3]
    ├────────────────────────────────────────────────────────────
RES │ Content-Length: 6
RES │ Content-Type:   text/plain; charset=utf-8
RES │ Date:           Tue, 21 Apr 2026 21:13:48 GMT
RES │
RES │ Handle

func Retry

func Retry(
	parent http.RoundTripper,
	timeout time.Duration,
	wait func(i int, resp *http.Response, err error) time.Duration,
) *retry

Retry on any network error or any HTTP status >=400.

It calls the provided callback to determine how long too wait after an error, retrying immediately if the returned duration is 0, or aborting if it's <0.

The http.Client.Timeout applies to the entire request chain (including all retries). The timeout parameter sets a timeout for every retry attempt. The timeout applies to the request only, not the waiting period. <=0 means there is no additional timeout.

The RetryRatelimit helper can be used to delay until the ratelimit resets.

Example

Code:play 

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"time"

	"zgo.at/transport"
)

func main() {
	var i int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if i < 1 {
			w.WriteHeader(500)
		}
		w.Write([]byte("Handle"))
		fmt.Printf("=> Handle attempt %d\n", i)
		i++
	}))
	defer srv.Close()

	c := http.Client{
		// The timeout applies to the entire request chain, including all
		// retries, so set this to an hour. The transport.Retry() function has a
		// timeout that applies to every individual retry attempt.
		Timeout: 1 * time.Hour,

		Transport: transport.Retry(http.DefaultTransport, 10*time.Second,
			func(i int, resp *http.Response, err error) time.Duration {
				if i > 10 { // Retry up to ten times.
					return -1
				}

				// Try to use Ratelimit headers first.
				d := transport.RetryRatelimit(0)(i, resp, err)
				if d == 0 {
					// Rate-Limit headers not present: use exponential backoff with
					// a maximum of 10 minutes.
					d = min(10*time.Minute, time.Duration(1<<i)*time.Second)
				}
				return d
			},
		),
	}

	resp, err := c.Get(srv.URL)
	if err != nil {
		log.Fatal(err)
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))

}

Output:

=> Handle attempt 0
=> Handle attempt 1
Handle

func RetryRatelimit

func RetryRatelimit(other time.Duration) func(_ int, resp *http.Response, err error) time.Duration

RetryRatelimit tries to determine the time to wait until the ratelimit resets.

other is used as the default if err != nil, if the status isn't 429 or 503, if there are no rate limit headers, or if there is any error parsing the headers.

Types

type CacheExpirer

type CacheExpirer interface {
	Expired(s CacheStorer, cached *http.Response, cachedErr error) bool
}

CacheExpirer determines if a cached response is expired.

type CacheStorer

type CacheStorer interface {
	// Get a stored cache entry. The error return is the cached error (if
	// any), not an error return for this function.
	Get(*http.Request) (*http.Response, error, bool)

	// Put stores a new cache Response and error for the request.
	Put(*http.Request, *http.Response, error)
}

CacheStorer is a cache storer.

type ErrHTTPError

type ErrHTTPError struct {
	StatusCode  int
	Status      string
	ContentType string
	Body        []byte
	FullBody    bool
}

func (ErrHTTPError) Error

func (e ErrHTTPError) Error() string

type LogOption

type LogOption uint64

Source Files

cache.go filter.go intercept.go log.go retry.go transport.go

Version
v0.0.0-20260422153143-00faf3971111 (latest)
Published
Apr 22, 2026
Platform
linux/amd64
Imports
22 packages
Last checked
2 weeks ago

Tools for package owners.