
An HTTP client for LLM APIs in Go: timeouts, limits, errors, and Retry-After
One shared client, deadlines on the context, size limits, errors inside a 200, and honoring Retry-After.
Stage 2: Call the modelA model API is an ordinary HTTPS endpoint with unusual traffic.
A reply can take half a minute to start, and a streamed one can take minutes to finish; the body can be large; rate limits are part of normal operation; and a request can fail after it has succeeded, with an error inside a 200 OK.
Go’s net/http handles all of it, with a few choices made deliberately.
This page goes through them, with code from the companion repository, where it is tested.
For the chat completions request itself, start with calling a model from Go; for when and how to retry, see retries and observability.
One client, shared
An http.Client and the http.Transport under it are safe for concurrent use (many goroutines can call them at the same time), and the transport keeps a pool of open connections.
Make one and share it; a new client or transport per request throws the pool away and pays for a new TCP and TLS handshake every time (the round trips that open a connection and set up encryption before any request is sent).
The model client takes an *http.Client as a field, and uses http.DefaultClient when it is nil:
// Client calls a model through OpenRouter.
type Client struct {
BaseURL string // DefaultBaseURL if empty
APIKey string // sent as a bearer token
Model string // a model ID from OpenRouter's list
HTTP *http.Client // http.DefaultClient if nil
Timeout time.Duration // per Generate call; 2 minutes if zero
// IdleTimeout is the longest a stream may go without sending a
// byte, including the wait for its headers; 30 seconds if zero
// (chapter 6). Stream doesn't use Timeout.
IdleTimeout time.Duration
// Providers, if set, are the only providers OpenRouter may send
// requests to, such as "novita" (chapter 3).
Providers []string
}
Taking the client as a field, instead of building one inside, lets the caller decide what it is: a client with a proxy, one shared by every agent in a process, or one whose transport replays recorded replies in tests.
The default transport is a sensible start: a 30-second dial timeout, a 10-second TLS handshake timeout, HTTP/2 where the server offers it, and idle connections kept for 90 seconds. One default matters when an agent runs many calls to one provider in parallel: the transport may keep too few idle connections.
Over HTTP/1.1, the default transport keeps at most two idle connections per host (DefaultMaxIdleConnsPerHost).
A program running many calls to one provider in parallel reopens connections constantly unless it uses a transport with a higher MaxIdleConnsPerHost.
Over HTTP/2, one connection carries many requests at once, and the limit matters less.
Deadlines go on the context
http.Client has a Timeout field, and it is the wrong tool here.
It covers the whole exchange, including reading the body, so it can’t tell a slow start from a long reply: a timeout long enough for a streamed reply that legitimately takes three minutes is far too long to notice a request that never got an answer.
The client instead puts a deadline on each call’s context, two minutes by default, and a stream gets an idle timer that measures silence instead of total time.
Both set a context cause (context.WithTimeoutCause), so the error from a timeout says which deadline ended the call.
The context page shows both.
Read with a limit
A response body is read with a cap, so a misbehaving server or proxy can’t make the client allocate without bound:
// readResponse reads and decodes a response, and checks it holds a
// reply.
func readResponse(ctx context.Context,
resp *http.Response) (chatResponse, error) {
data, err := io.ReadAll(io.LimitReader(resp.Body, maxBody+1))
if err != nil {
err = callError(ctx, err)
return chatResponse{}, fmt.Errorf("read response: %w", err)
}
if len(data) > maxBody {
return chatResponse{}, errTooLarge
}
if resp.StatusCode != http.StatusOK {
return chatResponse{}, statusError(resp, data)
}
var r chatResponse
if err := json.Unmarshal(data, &r); err != nil {
return chatResponse{}, fmt.Errorf("decode response: %w", err)
}
if e := r.Error; e != nil {
return chatResponse{}, e.asAPIError()
}
if len(r.Choices) == 0 {
return chatResponse{}, errNoChoices
}
if e := r.Choices[0].Error; e != nil {
return r, fmt.Errorf("%w: %w", agent.ErrProviderFailed,
e.asAPIError())
}
return r, nil
}
The limit is 16 MB, far above any reply, and the client reads one byte past it so it can tell a body that is exactly the limit from one that is over.
The checks after reading are in order, and each catches something real:
- A status other than 200 becomes an API error, built below.
- A 200 can still carry an
errorobject instead of a reply: the request was accepted, and the provider failed. - A reply with no choices is unusable.
- A choice can carry its own error, when a provider failed partway through writing it; the tokens it did write are still billed, so the reply is returned alongside the error.
A client that only checks the status code treats cases 2 and 4 as answers.
When the status isn’t 200
A refused request becomes an *APIError with the status, the API’s own message, and the wait the server asked for:
// statusError builds the APIError for a response that wasn't 200, using
// the API's message when the body has one.
func statusError(resp *http.Response, body []byte) *APIError {
e := &APIError{
Status: resp.StatusCode,
Message: "no details",
RetryAfter: agent.RetryAfter(resp.Header, time.Now()),
}
var b struct {
Error apiError `json:"error"`
}
switch {
case json.Unmarshal(body, &b) == nil && b.Error.Message != "":
e.Message = b.Error.Message
e.LimitSource = b.Error.Metadata.LimitSource
case len(body) > 0: // not the API's JSON, such as a proxy's page
e.Message = string(body[:min(len(body), 200)])
}
return e
}
The body isn’t always the API’s JSON. A proxy or load balancer in between can answer with an HTML error page, so the client keeps the first 200 bytes of whatever came back rather than failing to decode it.
The wait comes from the Retry-After header, which has two forms, a number of seconds or an HTTP date:
// RetryAfter reads a Retry-After header in either of its forms, a
// number of seconds or an HTTP date, as a wait from now. A wait that is
// negative, in the past, or unreadable is 0; one over a day is capped
// at a day, so the multiplication can't overflow (chapter 12).
func RetryAfter(h http.Header, now time.Time) time.Duration {
v := strings.TrimSpace(h.Get("Retry-After"))
if v == "" {
return 0
}
if secs, err := strconv.ParseInt(v, 10, 64); err == nil {
return time.Duration(min(max(secs, 0), 86400)) * time.Second
}
t, err := http.ParseTime(v)
if err != nil {
return 0
}
return min(max(t.Sub(now), 0), 24*time.Hour)
}
Both forms are clamped: a value in the past or one that doesn’t parse is no wait, and anything over a day is capped at a day.
A server’s header is input like any other, and a huge value multiplied into a time.Duration would overflow.
Honoring the wait, within limits
The retry policy uses the server’s wait when there is one, and gives up instead when the wait is longer than the caller is willing to spend.
The function that decides how long to wait before a retry:
// wait returns how long to wait before retry n, after err, or a
// *RetryError saying why to give up.
func (p Policy) wait(n int, d Decision, spent time.Duration,
err error) (time.Duration, error) {
give := func(why string) (time.Duration, error) {
return 0, &RetryError{n, fmt.Errorf("%s: %w", why, err)}
}
if n >= cmp.Or(p.Attempts, 4) {
return give("no attempts left")
}
if !p.RetryBudget.Allow() {
return give("retry budget spent")
}
wait := p.Backoff.Delay(n)
if d.After > 0 {
if d.After > cmp.Or(p.MaxWait, time.Minute) {
return give(fmt.Sprintf("asked to wait %v", d.After))
}
wait = d.After + p.Backoff.Delay(1)
}
if spent+wait > cmp.Or(p.Within, 5*time.Minute) {
return give("out of time")
}
if p.OnRetry != nil {
p.OnRetry(n, err, d, wait)
}
return wait, nil
}
A server’s wait replaces the backoff for that retry, plus a random extra of up to a second, so that many clients told to wait the same time don’t all come back in the same instant.
A wait longer than MaxWait, one minute by default, ends the retries with an error that names the wait, which is more useful than sleeping through it.
Which errors are worth retrying is decided by Classify, described on the errors page; the backoff itself is on the retries page.
Keep connections reusable
A connection goes back to the pool only when its response body has been read to the end and closed.
defer resp.Body.Close() after every successful request is the rule, and reading the whole body, as readResponse does, is what lets the connection be reused.
Code that gives up on a response early, and expects more requests to the same host, reads what is left with io.Copy(io.Discard, resp.Body) before closing it.
RoundTrippers: where tests and faults plug in
http.Client sends every request through its transport, an http.RoundTripper: an interface with one method, RoundTrip(*http.Request) (*http.Response, error).
Wrapping or replacing it changes how requests are sent without touching the client code.
The companion code uses that in two ways. Every program runs offline by default with a transport that answers from recorded replies, which testing offline describes. And the retry tests use a transport that injects failures, a status, a dropped connection, a body cut short, in front of a real server.
The transport that injects them:
// RoundTrip applies the next fault to req, or sends it on.
func (f *Faulty) RoundTrip(req *http.Request) (*http.Response, error) {
fault := f.next()
if fault.Drop || fault.Status != 0 {
return fault.instead(req)
}
resp, err := f.transport().RoundTrip(req)
switch {
case err != nil:
return nil, err //nolint:wrapcheck // passed on as is
case fault.Lost:
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return nil, badRecordMAC
case fault.Reset > 0:
resp.Body = &cutBody{resp.Body, fault.Reset, resetErr}
case fault.Truncate > 0:
resp.Body = &cutBody{resp.Body, fault.Truncate,
io.ErrUnexpectedEOF}
}
return resp, nil
}
When it fakes a lost connection after a real response, it drains and closes that response’s body first, so the real connection isn’t leaked.
A RoundTripper owns the request body it is given, too: one that answers without sending a request closes the body itself.
Mistakes to avoid
- A new
http.Clientorhttp.Transportper request. Each one starts a new connection pool; share one. http.Client.Timeouton streamed replies. It counts the whole stream; use a deadline per call on the context, and an idle timer for streams.io.ReadAll(resp.Body)with no limit. Cap it withio.LimitReader.- Checking only the status code. A model API can report a failure inside a
200. - An unclosed body. The connection is never reused, and a long-running agent runs out of file descriptors.
- Trusting
Retry-Afterwithout a cap. Clamp it, and decide how long you’ll wait before you sleep. - The API key anywhere but a header. A failed request’s error includes its URL, so a key in the query string ends up in error messages and logs; and never log request headers.
Try it
From the code directory of a clone of the companion repository:
$ go test ./ch12/agent -run 'TestDoHonorsRetryAfter|TestFaulty' -v
$ go test ./ch06/openrouter -run 'TestErrorInsideA200|TestAPIErrors' -v
- In
TestTimeoutSynctest(ch06/openrouter/client_test.go), give thehttp.ClientaTimeoutof one second and see which deadline ends the call, and what the error says. TestAPIErrorEdgesinch06/openrouter/client_test.goalready points the client at anhttptestserver that returns a502with an HTML body; run it withgo test ./ch06/openrouter -run TestAPIErrorEdges -v, then change the body or the status in the test and read the*APIErrorit produces.- Give
RetryAftera date in the past, an empty header, and999999999, and check each result.
type Client struct {
BaseURL string // DefaultBaseURL if empty
APIKey string // sent as a bearer token
Model string // a model ID from OpenRouter's list
HTTP *http.Client // http.DefaultClient if nil
Timeout time.Duration // per Generate call; 2 minutes if zero
// IdleTimeout is the longest a stream may go without sending a
// byte, including the wait for its headers; 30 seconds if zero
// (chapter 6). Stream doesn't use Timeout.
// ...
}
Client calls a model through OpenRouter.
Client calls a model through OpenRouter. type Client struct { BaseURL string // DefaultBaseURL if empty APIKey string // sent as a bearer token Model string // a model ID from OpenRouter's list HTTP *http.Client // http.DefaultClient if nil Timeout time.Duration // per Generate call; 2 minutes if zero // IdleTimeout is the longest a stream may go without sending a // byte, including the wait for its headers; 30 seconds if zero // (chapter 6). Stream doesn't use Timeout. // ... }. In ch12/openrouter/client.go, line 47.
field BaseURL string
DefaultBaseURL if empty
DefaultBaseURL if empty field BaseURL string. In ch12/openrouter/client.go, line 48.
type string string
string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text.
string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. type string string. Documented at pkg.go.dev, builtin.string.
field APIKey string
sent as a bearer token
sent as a bearer token field APIKey string. In ch12/openrouter/client.go, line 49.
field Model string
a model ID from OpenRouter's list
a model ID from OpenRouter's list field Model string. In ch12/openrouter/client.go, line 50.
field HTTP *http.Client
http.DefaultClient if nil
http.DefaultClient if nil field HTTP *http.Client. In ch12/openrouter/client.go, line 51.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
type Client struct {
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
// CheckRedirect specifies the policy for handling redirects.
// If CheckRedirect is not nil, the client calls it before
// following an HTTP redirect. The arguments req and via are
// the upcoming request and the requests made already, oldest
// ...
}
A Client is an HTTP client.
A Client is an HTTP client. type Client struct { // Transport specifies the mechanism by which individual // HTTP requests are made. // If nil, DefaultTransport is used. Transport RoundTripper // CheckRedirect specifies the policy for handling redirects. // If CheckRedirect is not nil, the client calls it before // following an HTTP redirect. The arguments req and via are // the upcoming request and the requests made already, oldest // ... }. Documented at pkg.go.dev, net/http.Client.
field Timeout time.Duration
per Generate call; 2 minutes if zero
per Generate call; 2 minutes if zero field Timeout time.Duration. In ch12/openrouter/client.go, line 52.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
type Duration int64
A Duration represents the elapsed time between two instants as an int64 nanosecond count.
A Duration represents the elapsed time between two instants as an int64 nanosecond count. type Duration int64. Documented at pkg.go.dev, time.Duration.
field IdleTimeout time.Duration
IdleTimeout is the longest a stream may go without sending a byte, including the wait for its headers; 30 seconds if zero (chapter 6).
IdleTimeout is the longest a stream may go without sending a byte, including the wait for its headers; 30 seconds if zero (chapter 6). field IdleTimeout time.Duration. In ch12/openrouter/client.go, line 57.
field Providers []string
Providers, if set, are the only providers OpenRouter may send requests to, such as "novita" (chapter 3).
Providers, if set, are the only providers OpenRouter may send requests to, such as "novita" (chapter 3). field Providers []string. In ch12/openrouter/client.go, line 61.
func readResponse(ctx context.Context,
resp *http.Response) (chatResponse, error)
readResponse reads and decodes a response, and checks it holds a reply.
readResponse reads and decodes a response, and checks it holds a reply. func readResponse(ctx context.Context, resp *http.Response) (chatResponse, error). In ch12/openrouter/client.go, line 183.
var ctx context.Context
var ctx context.Context. In ch12/openrouter/client.go, line 183.
package context ("context")
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. package context ("context"). Documented at pkg.go.dev, context.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// ...
}
A Context carries a deadline, a cancellation signal, and other values across API boundaries.
A Context carries a deadline, a cancellation signal, and other values across API boundaries. type Context interface { // Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results. Deadline() (deadline time.Time, ok bool) // Done returns a channel that's closed when work done on behalf of this // context should be canceled. Done may return nil if this context can // never be canceled. Successive calls to Done return the same value. // The close of the Done channel may happen asynchronously, // ... }. Documented at pkg.go.dev, context.Context.
var resp *http.Response
var resp *http.Response. In ch12/openrouter/client.go, line 184.
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
// Header maps header keys to values. If the response had multiple
// headers with the same key, they may be concatenated, with comma
// delimiters. (RFC 7230, section 3.2.2 requires that multiple headers
// ...
}
Response represents the response from an HTTP request.
Response represents the response from an HTTP request. type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 Proto string // e.g. "HTTP/1.0" ProtoMajor int // e.g. 1 ProtoMinor int // e.g. 0 // Header maps header keys to values. If the response had multiple // headers with the same key, they may be concatenated, with comma // delimiters. (RFC 7230, section 3.2.2 requires that multiple headers // ... }. Documented at pkg.go.dev, net/http.Response.
type chatResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Provider string `json:"provider"`
Choices []chatChoice `json:"choices"`
Usage chatUsage `json:"usage"`
Error *apiError `json:"error"`
}
type chatResponse struct { ID string `json:"id"` Model string `json:"model"` Provider string `json:"provider"` Choices []chatChoice `json:"choices"` Usage chatUsage `json:"usage"` Error *apiError `json:"error"` }. In ch12/openrouter/wire.go, line 169.
type error interface {
Error() string
}
The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.
The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error. type error interface { Error() string }. Documented at pkg.go.dev, builtin.error.
var data []byte
var data []byte. In ch12/openrouter/client.go, line 185.
package io ("io")
Package io provides basic interfaces to I/O primitives.
Package io provides basic interfaces to I/O primitives. package io ("io"). Documented at pkg.go.dev, io.
func ReadAll(r Reader) ([]byte, error)
ReadAll reads from r until an error or EOF and returns the data it read.
ReadAll reads from r until an error or EOF and returns the data it read. func ReadAll(r Reader) ([]byte, error). Documented at pkg.go.dev, io.ReadAll.
func LimitReader(r Reader, n int64) Reader
LimitReader returns a Reader that reads from r but stops with EOF after n bytes.
LimitReader returns a Reader that reads from r but stops with EOF after n bytes. func LimitReader(r Reader, n int64) Reader. Documented at pkg.go.dev, io.LimitReader.
field Body io.ReadCloser
Body represents the response body.
Body represents the response body. field Body io.ReadCloser. Documented at pkg.go.dev, net/http.Response.Body.
const maxBody = 16 << 20
maxBody is the most of a response the client reads: far more than any reply, but a limit on a broken server.
maxBody is the most of a response the client reads: far more than any reply, but a limit on a broken server. const maxBody = 16 << 20. In ch12/openrouter/client.go, line 42.
var nil Type
nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.
nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type. var nil Type. Documented at pkg.go.dev, builtin.nil.
func callError(ctx context.Context, err error) error
callError makes the error from a cancelled call carry both the kind of cancellation and its cause.
callError makes the error from a cancelled call carry both the kind of cancellation and its cause. func callError(ctx context.Context, err error) error. In ch12/openrouter/client.go, line 270.
package fmt ("fmt")
Package fmt implements formatted I/O with functions analogous to C's printf and scanf.
Package fmt implements formatted I/O with functions analogous to C's printf and scanf. package fmt ("fmt"). Documented at pkg.go.dev, fmt.
func Errorf(format string, a ...any) (err error)
Errorf formats according to a format specifier and returns the string as a value that satisfies error.
Errorf formats according to a format specifier and returns the string as a value that satisfies error. func Errorf(format string, a ...any) (err error). Documented at pkg.go.dev, fmt.Errorf.
func len(v Type) int
The len built-in function returns the length of v, according to its type:
The len built-in function returns the length of v, according to its type: func len(v Type) int. Documented at pkg.go.dev, builtin.len.
var errTooLarge = errors.New("openrouter: response too large")
var errTooLarge = errors.New("openrouter: response too large"). In ch12/openrouter/client.go, line 37.
field StatusCode int
e.g.
e.g. field StatusCode int. Documented at pkg.go.dev, net/http.Response.StatusCode.
const StatusOK = 200
RFC 9110, 15.3.1
RFC 9110, 15.3.1 const StatusOK = 200. Documented at pkg.go.dev, net/http.StatusOK.
func statusError(resp *http.Response, body []byte) *APIError
statusError builds the APIError for a response that wasn't 200, using the API's message when the body has one.
statusError builds the APIError for a response that wasn't 200, using the API's message when the body has one. func statusError(resp *http.Response, body []byte) *APIError. In ch12/openrouter/errors.go, line 69.
var r chatResponse
var r chatResponse. In ch12/openrouter/client.go, line 196.
package json ("encoding/json/v2")
Package json implements semantic processing of JSON as specified in RFC 8259.
Package json implements semantic processing of JSON as specified in RFC 8259. package json ("encoding/json/v2"). Documented at pkg.go.dev, encoding/json/v2.
func Unmarshal(in []byte, out any, opts ...Options) (err error)
Unmarshal decodes a []byte input into a Go value according to the provided unmarshal and decode options (while ignoring marshal or encode options).
Unmarshal decodes a []byte input into a Go value according to the provided unmarshal and decode options (while ignoring marshal or encode options). func Unmarshal(in []byte, out any, opts ...Options) (err error). Documented at pkg.go.dev, encoding/json/v2.Unmarshal.
var e *apiError
var e *apiError. In ch12/openrouter/client.go, line 200.
field Error *apiError `json:"error"`
field Error *apiError `json:"error"`. In ch12/openrouter/wire.go, line 175.
func (e *apiError) asAPIError() *APIError
asAPIError turns an error object from inside a reply into an *APIError, with the status its code names, or 502 when the code isn't a number, so the classifier can tell a provider that failed with a 429 from one that refused the request (chapter 12).
asAPIError turns an error object from inside a reply into an *APIError, with the status its code names, or 502 when the code isn't a number, so the classifier can tell a provider that failed with a 429 from one that refused the request (chapter 12). func (e *apiError) asAPIError() *APIError. In ch12/openrouter/wire.go, line 227.
field Choices []chatChoice `json:"choices"`
field Choices []chatChoice `json:"choices"`. In ch12/openrouter/wire.go, line 173.
var errNoChoices = errors.New("openrouter: reply has no choices")
var errNoChoices = errors.New("openrouter: reply has no choices"). In ch12/openrouter/client.go, line 36.
var e *apiError
var e *apiError. In ch12/openrouter/client.go, line 206.
field Error *apiError `json:"error"`
a failure partway through
a failure partway through field Error *apiError `json:"error"`. In ch12/openrouter/wire.go, line 185.
package agent ("github.com/blackwell-systems/go-agents-book/code/ch12/agent")
Package agent is the agent loop and the types it works with.
Package agent is the agent loop and the types it works with. package agent ("github.com/blackwell-systems/go-agents-book/code/ch12/agent"). In ch12/agent.
var ErrProviderFailed = fmt.Errorf("%w: provider failed partway", ...
var ErrProviderFailed = fmt.Errorf("%w: provider failed partway", .... In ch12/agent/errors.go, line 26.
var resp *http.Response
var resp *http.Response. In ch12/openrouter/errors.go, line 69.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
type byte = uint8
byte is an alias for uint8 and is equivalent to uint8 in all ways.
byte is an alias for uint8 and is equivalent to uint8 in all ways. type byte = uint8. Documented at pkg.go.dev, builtin.byte.
type APIError struct {
Status int // HTTP status, or the code in the reply
Message string // the API's own explanation
RetryAfter time.Duration // the wait the API asked for, or 0
// LimitSource is the error's metadata.limit_source, which says which
// limit a 402 or 429 hit (chapter 12).
LimitSource string
}
An APIError is a request the API refused, or a provider failure it reported.
An APIError is a request the API refused, or a provider failure it reported. type APIError struct { Status int // HTTP status, or the code in the reply Message string // the API's own explanation RetryAfter time.Duration // the wait the API asked for, or 0 // LimitSource is the error's metadata.limit_source, which says which // limit a 402 or 429 hit (chapter 12). LimitSource string }. In ch12/openrouter/errors.go, line 17.
field Status int
HTTP status, or the code in the reply
HTTP status, or the code in the reply field Status int. In ch12/openrouter/errors.go, line 18.
field Message string
the API's own explanation
the API's own explanation field Message string. In ch12/openrouter/errors.go, line 19.
field RetryAfter time.Duration
the wait the API asked for, or 0
the wait the API asked for, or 0 field RetryAfter time.Duration. In ch12/openrouter/errors.go, line 20.
package agent ("github.com/blackwell-systems/go-agents-book/code/ch12/agent")
Package agent is the agent loop and the types it works with.
Package agent is the agent loop and the types it works with. package agent ("github.com/blackwell-systems/go-agents-book/code/ch12/agent"). In ch12/agent.
func RetryAfter(h http.Header, now time.Time) time.Duration
RetryAfter reads a Retry-After header in either of its forms, a number of seconds or an HTTP date, as a wait from now.
RetryAfter reads a Retry-After header in either of its forms, a number of seconds or an HTTP date, as a wait from now. func RetryAfter(h http.Header, now time.Time) time.Duration. In ch12/agent/retryafter.go, line 16.
field Header Header
Header maps header keys to values.
Header maps header keys to values. field Header Header. Documented at pkg.go.dev, net/http.Response.Header.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
func Now() Time
Now returns the current local time.
Now returns the current local time. func Now() Time. Documented at pkg.go.dev, time.Now.
var b struct{Error apiError "json:\"error\""}
var b struct{Error apiError "json:\"error\""}. In ch12/openrouter/errors.go, line 75.
field Error apiError
field Error apiError. In ch12/openrouter/errors.go, line 76.
type apiError struct {
Code jsontext.Value `json:"code"` // a number, or a string in streams
Message string `json:"message"`
Metadata struct {
LimitSource string `json:"limit_source"`
} `json:"metadata"`
}
apiError is the error object the API sends, in an error response or inside a 200 response when a provider fails.
apiError is the error object the API sends, in an error response or inside a 200 response when a provider fails. type apiError struct { Code jsontext.Value `json:"code"` // a number, or a string in streams Message string `json:"message"` Metadata struct { LimitSource string `json:"limit_source"` } `json:"metadata"` }. In ch12/openrouter/wire.go, line 215.
package json ("encoding/json/v2")
Package json implements semantic processing of JSON as specified in RFC 8259.
Package json implements semantic processing of JSON as specified in RFC 8259. package json ("encoding/json/v2"). Documented at pkg.go.dev, encoding/json/v2.
field Message string `json:"message"`
field Message string `json:"message"`. In ch12/openrouter/wire.go, line 217.
field LimitSource string
LimitSource is the error's metadata.limit_source, which says which limit a 402 or 429 hit (chapter 12).
LimitSource is the error's metadata.limit_source, which says which limit a 402 or 429 hit (chapter 12). field LimitSource string. In ch12/openrouter/errors.go, line 24.
field Metadata struct {
LimitSource string `json:"limit_source"`
} `json:"metadata"`
field Metadata struct { LimitSource string `json:"limit_source"` } `json:"metadata"`. In ch12/openrouter/wire.go, line 218.
field LimitSource string
field LimitSource string. In ch12/openrouter/wire.go, line 219.
func min[T cmp.Ordered](x T, y ...T) T
The min built-in function returns the smallest value of a fixed number of arguments of cmp.Ordered types.
The min built-in function returns the smallest value of a fixed number of arguments of cmp.Ordered types. func min[T cmp.Ordered](x T, y ...T) T. Documented at pkg.go.dev, builtin.min.
var h http.Header
var h http.Header. In ch12/agent/retryafter.go, line 16.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
type Header map[string][]string
A Header represents the key-value pairs in an HTTP header.
A Header represents the key-value pairs in an HTTP header. type Header map[string][]string. Documented at pkg.go.dev, net/http.Header.
var now time.Time
var now time.Time. In ch12/agent/retryafter.go, line 16.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// ...
}
A Time represents an instant in time with nanosecond precision.
A Time represents an instant in time with nanosecond precision. type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // ... }. Documented at pkg.go.dev, time.Time.
package strings ("strings")
Package strings implements simple functions to manipulate UTF-8 encoded strings.
Package strings implements simple functions to manipulate UTF-8 encoded strings. package strings ("strings"). Documented at pkg.go.dev, strings.
func TrimSpace(s string) string
TrimSpace returns a slice (substring) of the string s, with all leading and trailing white space removed, as defined by Unicode.
TrimSpace returns a slice (substring) of the string s, with all leading and trailing white space removed, as defined by Unicode. func TrimSpace(s string) string. Documented at pkg.go.dev, strings.TrimSpace.
func (h Header) Get(key string) string
Get gets the first value associated with the given key.
Get gets the first value associated with the given key. func (h Header) Get(key string) string. Documented at pkg.go.dev, net/http.Header.Get.
package strconv ("strconv")
Package strconv implements conversions to and from string representations of basic data types.
Package strconv implements conversions to and from string representations of basic data types. package strconv ("strconv"). Documented at pkg.go.dev, strconv.
func ParseInt(s string, base int, bitSize int) (i int64, err error)
ParseInt interprets a string s in the given base (0, 2 to 36) and bit size (0 to 64) and returns the corresponding value i.
ParseInt interprets a string s in the given base (0, 2 to 36) and bit size (0 to 64) and returns the corresponding value i. func ParseInt(s string, base int, bitSize int) (i int64, err error). Documented at pkg.go.dev, strconv.ParseInt.
func max[T cmp.Ordered](x T, y ...T) T
The max built-in function returns the largest value of a fixed number of arguments of cmp.Ordered types.
The max built-in function returns the largest value of a fixed number of arguments of cmp.Ordered types. func max[T cmp.Ordered](x T, y ...T) T. Documented at pkg.go.dev, builtin.max.
const Second = 1000 * Millisecond
const Second = 1000 * Millisecond. Documented at pkg.go.dev, time.Second.
func ParseTime(text string) (t time.Time, err error)
ParseTime parses a time header (such as the Date: header), trying each of the three formats allowed by HTTP/1.1: [TimeFormat], time.RFC850, and time.ANSIC.
ParseTime parses a time header (such as the Date: header), trying each of the three formats allowed by HTTP/1.1: [TimeFormat], time.RFC850, and time.ANSIC. func ParseTime(text string) (t time.Time, err error). Documented at pkg.go.dev, net/http.ParseTime.
func (t Time) Sub(u Time) Duration
Sub returns the duration t-u.
Sub returns the duration t-u. func (t Time) Sub(u Time) Duration. Documented at pkg.go.dev, time.Time.Sub.
type Policy struct {
Attempts int // tries, the first included; 0 means 4
Within time.Duration // start no retry after this; 0 means 5m
Each time.Duration // one attempt's deadline; 0 means none
MaxWait time.Duration // longest server-asked wait; 0 means 1m
Backoff Backoff
RetryBudget *RetryBudget // shared with other runs, or nil
Breaker *Breaker // shared with other runs, or nil
// ...
}
A Policy says how hard Do tries.
A Policy says how hard Do tries. type Policy struct { Attempts int // tries, the first included; 0 means 4 Within time.Duration // start no retry after this; 0 means 5m Each time.Duration // one attempt's deadline; 0 means none MaxWait time.Duration // longest server-asked wait; 0 means 1m Backoff Backoff RetryBudget *RetryBudget // shared with other runs, or nil Breaker *Breaker // shared with other runs, or nil // ... }. In ch12/agent/retry.go, line 16.
func (p Policy) wait(n int, d Decision, spent time.Duration,
err error) (time.Duration, error)
wait returns how long to wait before retry n, after err, or a *RetryError saying why to give up.
wait returns how long to wait before retry n, after err, or a *RetryError saying why to give up. func (p Policy) wait(n int, d Decision, spent time.Duration, err error) (time.Duration, error). In ch12/agent/retry.go, line 182.
type int int
int is a signed integer type that is at least 32 bits in size.
int is a signed integer type that is at least 32 bits in size. type int int. Documented at pkg.go.dev, builtin.int.
type Decision struct {
Retry bool
After time.Duration // the server's requested delay, or 0
Why string // a word for logs and traces
}
A Decision says whether to send a failed call again, and how long the server asked us to wait first.
A Decision says whether to send a failed call again, and how long the server asked us to wait first. type Decision struct { Retry bool After time.Duration // the server's requested delay, or 0 Why string // a word for logs and traces }. In ch12/agent/classify.go, line 19.
var spent time.Duration
var spent time.Duration. In ch12/agent/retry.go, line 182.
package time ("time")
Package time provides functionality for measuring and displaying time.
Package time provides functionality for measuring and displaying time. package time ("time"). Documented at pkg.go.dev, time.
var give func(why string) (time.Duration, error)
var give func(why string) (time.Duration, error). In ch12/agent/retry.go, line 184.
type RetryError struct {
Attempts int
Err error
}
A RetryError is the last error of a call that was retried until the policy gave up.
A RetryError is the last error of a call that was retried until the policy gave up. type RetryError struct { Attempts int Err error }. In ch12/agent/retry.go, line 44.
package fmt ("fmt")
Package fmt implements formatted I/O with functions analogous to C's printf and scanf.
Package fmt implements formatted I/O with functions analogous to C's printf and scanf. package fmt ("fmt"). Documented at pkg.go.dev, fmt.
package cmp ("cmp")
Package cmp provides types and functions related to comparing ordered values.
Package cmp provides types and functions related to comparing ordered values. package cmp ("cmp"). Documented at pkg.go.dev, cmp.
func Or[T comparable](vals ...T) T
Or returns the first of its arguments that is not equal to the zero value.
Or returns the first of its arguments that is not equal to the zero value. func Or[T comparable](vals ...T) T. Documented at pkg.go.dev, cmp.Or.
field Attempts int
tries, the first included; 0 means 4
tries, the first included; 0 means 4 field Attempts int. In ch12/agent/retry.go, line 17.
field RetryBudget *RetryBudget
shared with other runs, or nil
shared with other runs, or nil field RetryBudget *RetryBudget. In ch12/agent/retry.go, line 23.
func (b *RetryBudget) Allow() bool
Allow reports whether a retry may be sent now.
Allow reports whether a retry may be sent now. func (b *RetryBudget) Allow() bool. In ch12/agent/budget.go, line 27.
var wait time.Duration
var wait time.Duration. In ch12/agent/retry.go, line 193.
func (b Backoff) Delay(n int) time.Duration
Delay returns the wait before retry n, counting from 1.
Delay returns the wait before retry n, counting from 1. func (b Backoff) Delay(n int) time.Duration. In ch12/agent/backoff.go, line 25.
field After time.Duration
the server's requested delay, or 0
the server's requested delay, or 0 field After time.Duration. In ch12/agent/classify.go, line 21.
field MaxWait time.Duration
longest server-asked wait; 0 means 1m
longest server-asked wait; 0 means 1m field MaxWait time.Duration. In ch12/agent/retry.go, line 20.
const Minute = 60 * Second
const Minute = 60 * Second. Documented at pkg.go.dev, time.Minute.
func Sprintf(format string, a ...any) string
Sprintf formats according to a format specifier and returns the resulting string.
Sprintf formats according to a format specifier and returns the resulting string. func Sprintf(format string, a ...any) string. Documented at pkg.go.dev, fmt.Sprintf.
field Within time.Duration
start no retry after this; 0 means 5m
start no retry after this; 0 means 5m field Within time.Duration. In ch12/agent/retry.go, line 18.
field OnRetry func(n int, err error, d Decision, wait time.Duration)
OnRetry, if set, is told about each retry before its wait, for logs and traces.
OnRetry, if set, is told about each retry before its wait, for logs and traces. field OnRetry func(n int, err error, d Decision, wait time.Duration). In ch12/agent/retry.go, line 33.
type Faulty struct {
Next http.RoundTripper // http.DefaultTransport if nil
Faults []Fault
mu sync.Mutex
n int // requests seen
}
Faulty is an http.RoundTripper that injects one Fault per request, in order, and sends requests on to Next once its faults run out or when a fault is the zero Fault.
Faulty is an http.RoundTripper that injects one Fault per request, in order, and sends requests on to Next once its faults run out or when a fault is the zero Fault. type Faulty struct { Next http.RoundTripper // http.DefaultTransport if nil Faults []Fault mu sync.Mutex n int // requests seen }. In ch12/agent/fault.go, line 30.
func (f *Faulty) RoundTrip(req *http.Request) (*http.Response, error)
RoundTrip applies the next fault to req, or sends it on.
RoundTrip applies the next fault to req, or sends it on. func (f *Faulty) RoundTrip(req *http.Request) (*http.Response, error). In ch12/agent/fault.go, line 48.
package http ("net/http")
Package http provides HTTP client and server implementations.
Package http provides HTTP client and server implementations. package http ("net/http"). Documented at pkg.go.dev, net/http.
type Request struct {
// Method specifies the HTTP method (GET, POST, PUT, etc.).
// For client requests, an empty string means GET.
Method string
// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI. For
// ...
}
A Request represents an HTTP request received by a server or to be sent by a client.
A Request represents an HTTP request received by a server or to be sent by a client. type Request struct { // Method specifies the HTTP method (GET, POST, PUT, etc.). // For client requests, an empty string means GET. Method string // URL specifies either the URI being requested (for server // requests) or the URL to access (for client requests). // // For server requests, the URL is parsed from the URI // supplied on the Request-Line as stored in RequestURI. For // ... }. Documented at pkg.go.dev, net/http.Request.
func (f *Faulty) next() Fault
next returns the fault for the next request.
next returns the fault for the next request. func (f *Faulty) next() Fault. In ch12/agent/fault.go, line 95.
field Drop bool
fail before the request leaves
fail before the request leaves field Drop bool. In ch12/agent/fault.go, line 20.
field Status int
answer with this status instead of sending
answer with this status instead of sending field Status int. In ch12/agent/fault.go, line 17.
func (fault Fault) instead(req *http.Request) (*http.Response, error)
instead answers req without sending it: with fault's status, or as a dropped connection.
instead answers req without sending it: with fault's status, or as a dropped connection. func (fault Fault) instead(req *http.Request) (*http.Response, error). In ch12/agent/fault.go, line 74.
var resp *http.Response
var resp *http.Response. In ch12/agent/fault.go, line 53.
func (f *Faulty) transport() http.RoundTripper
func (f *Faulty) transport() http.RoundTripper. In ch12/agent/fault.go, line 105.
func (RoundTripper) RoundTrip(*Request) (*Response, error)
RoundTrip executes a single HTTP transaction, returning a Response for the provided Request.
net/http.RoundTripper.RoundTrip
RoundTrip executes a single HTTP transaction, returning a Response for the provided Request. func (RoundTripper) RoundTrip(*Request) (*Response, error). Documented at pkg.go.dev, net/http.RoundTripper.RoundTrip.
field Lost bool
send, let the server answer, then fail
send, let the server answer, then fail field Lost bool. In ch12/agent/fault.go, line 21.
package io ("io")
Package io provides basic interfaces to I/O primitives.
Package io provides basic interfaces to I/O primitives. package io ("io"). Documented at pkg.go.dev, io.
func Copy(dst Writer, src Reader) (written int64, err error)
Copy copies from src to dst until either EOF is reached on src or an error occurs.
Copy copies from src to dst until either EOF is reached on src or an error occurs. func Copy(dst Writer, src Reader) (written int64, err error). Documented at pkg.go.dev, io.Copy.
var Discard Writer = discard{}
Discard is a [Writer] on which all Write calls succeed without doing anything.
Discard is a [Writer] on which all Write calls succeed without doing anything. var Discard Writer = discard{}. Documented at pkg.go.dev, io.Discard.
func (Closer) Close() error
func (Closer) Close() error. Documented at pkg.go.dev, io.Closer.Close.
var badRecordMAC = &net.OpError{Op: "remote error", ...
badRecordMAC is the error chapter 8's run stopped with: a TLS alert from the server, as crypto/tls reports it.
badRecordMAC is the error chapter 8's run stopped with: a TLS alert from the server, as crypto/tls reports it. var badRecordMAC = &net.OpError{Op: "remote error", .... In ch12/agent/fault.go, line 40.
field Reset int
send, then reset the body after this many bytes
send, then reset the body after this many bytes field Reset int. In ch12/agent/fault.go, line 22.
type cutBody struct {
io.ReadCloser
left int
err error
}
cutBody passes through left bytes of a body, then fails with err.
cutBody passes through left bytes of a body, then fails with err. type cutBody struct { io.ReadCloser left int err error }. In ch12/agent/fault.go, line 120.
var resetErr = &net.OpError{Op: "read", Net: "tcp", ...
resetErr is how a connection reset reaches a reader.
resetErr is how a connection reset reaches a reader. var resetErr = &net.OpError{Op: "read", Net: "tcp", .... In ch12/agent/fault.go, line 91.
field Truncate int
send, then end the body early after this many bytes
send, then end the body early after this many bytes field Truncate int. In ch12/agent/fault.go, line 23.
var ErrUnexpectedEOF = errors.New("unexpected EOF")
ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size block or data structure.
ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size block or data structure. var ErrUnexpectedEOF = errors.New("unexpected EOF"). Documented at pkg.go.dev, io.ErrUnexpectedEOF.
type Transport struct {
idleMu sync.Mutex
closeIdle bool // user has requested to close all idle conns
idleConn map[connectMethodKey][]*persistConn // most recently used at end
idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns
idleLRU connLRU
reqMu sync.Mutex
reqCanceler map[*Request]context.CancelCauseFunc
// ...
}
Transport is an implementation of [RoundTripper] that supports HTTP, HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT).
Transport is an implementation of [RoundTripper] that supports HTTP, HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT). type Transport struct { idleMu sync.Mutex closeIdle bool // user has requested to close all idle conns idleConn map[connectMethodKey][]*persistConn // most recently used at end idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns idleLRU connLRU reqMu sync.Mutex reqCanceler map[*Request]context.CancelCauseFunc // ... }. Documented at pkg.go.dev, net/http.Transport.
var DefaultClient = &Client{}
DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
DefaultClient is the default [Client] and is used by [Get], [Head], and [Post]. var DefaultClient = &Client{}. Documented at pkg.go.dev, net/http.DefaultClient.
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
WithTimeoutCause behaves like [WithTimeout] but also sets the cause of the returned Context when the timeout expires.
WithTimeoutCause behaves like [WithTimeout] but also sets the cause of the returned Context when the timeout expires. func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc). Documented at pkg.go.dev, context.WithTimeoutCause.
type RoundTripper interface {
// RoundTrip executes a single HTTP transaction, returning
// a Response for the provided Request.
//
// RoundTrip should not attempt to interpret the response. In
// particular, RoundTrip must return err == nil if it obtained
// a response, regardless of the response's HTTP status code.
// A non-nil err should be reserved for failure to obtain a
// response. Similarly, RoundTrip should not attempt to
// handle higher-level protocol details such as redirects,
// ...
}
RoundTripper is an interface representing the ability to execute a single HTTP transaction, obtaining the [Response] for a given [Request].
RoundTripper is an interface representing the ability to execute a single HTTP transaction, obtaining the [Response] for a given [Request]. type RoundTripper interface { // RoundTrip executes a single HTTP transaction, returning // a Response for the provided Request. // // RoundTrip should not attempt to interpret the response. In // particular, RoundTrip must return err == nil if it obtained // a response, regardless of the response's HTTP status code. // A non-nil err should be reserved for failure to obtain a // response. Similarly, RoundTrip should not attempt to // handle higher-level protocol details such as redirects, // ... }. Documented at pkg.go.dev, net/http.RoundTripper.
field Timeout time.Duration
Timeout specifies a time limit for requests made by this Client.
Timeout specifies a time limit for requests made by this Client. field Timeout time.Duration. Documented at pkg.go.dev, net/http.Client.Timeout.