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 ¶
- Constants
- Variables
- func Cache(parent http.RoundTripper, s CacheStorer, e CacheExpirer) *cache
- func CacheExpireTime(d time.Duration) *expireTime
- func CacheFile(path string) *cacheFile
- func CacheMemory() *cacheMemory
- func CacheNop() *cacheNop
- func Filter(parent http.RoundTripper, allow func(*url.URL) (bool, error)) *filter
- func FilterLocal(u *url.URL) (bool, error)
- func HTTPError(only500 bool, bodyLimit int) func(resp *http.Response, err error) (*http.Response, error)
- func Intercept(parent http.RoundTripper, fn func(*http.Response, error) (*http.Response, error)) *intercept
- func Log(parent http.RoundTripper, out io.Writer, what LogOption) *log
- func Retry( parent http.RoundTripper, timeout time.Duration, wait func(i int, resp *http.Response, err error) time.Duration, ) *retry
- func RetryRatelimit(other time.Duration) func(_ int, resp *http.Response, err error) time.Duration
- type CacheExpirer
- type CacheStorer
- type ErrHTTPError
- type LogOption
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.
Code:play
Output:Example¶
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))
}
=> 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 ¶
Filter requests.
Requests return ErrFiltered if allow returns false. It can optionally
return an error with some additional information.
Code:play
Output:Example¶
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)
}
Get "http://localhost/test": transport.Filter: request not allowed: FilterLocal: "localhost" is not allowed
true
func FilterLocal ¶
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.
Code:play
Output:Example¶
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"`))
}
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.
Code:play
Output:Example¶
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)
}
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.
Code:play
Output:Example¶
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))
}
=> Handle attempt 0
=> Handle attempt 1
Handle
func RetryRatelimit ¶
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.