Documentation
¶
Overview ¶
Package jsonapi is the one transport every Truestamp JSON:API client shares: the authenticated request carrying the tenant header, the response size cap, the one retry of a rate-limited request, and the classification of the error envelope into a small set of sentinels a command can errors.Is. The resource packages (internal/beacons, internal/blocks, internal/teams, internal/items, internal/proof) own only their routes and their decoding; each used to carry its own copy of this file, and the copies had started to differ in which statuses mapped to which class.
Index ¶
- Constants
- Variables
- func Do(ctx context.Context, cfg Config, method, path string, body []byte) ([]byte, error)
- func DoRaw(ctx context.Context, cfg Config, method, path string, body []byte) (*http.Response, []byte, error)
- func Get(ctx context.Context, cfg Config, path string) ([]byte, error)
- func Post(ctx context.Context, cfg Config, path string, payload any) ([]byte, error)
- func Send(req *http.Request) (*http.Response, []byte, error)
- func SetPageQuery(q url.Values, limit int, after, before string, count bool)
- func SetRateLimitNotifier(fn func(wait time.Duration, limit int64))
- func SortByID(oldestFirst bool) string
- type APIError
- type Config
- type PageInfo
Constants ¶
const CodeInvalidKeyset = "invalid_keyset"
CodeInvalidKeyset is the server's code for a page[after] / page[before] value that is not a cursor it issued.
const CodeRateLimited = "rate_limited"
CodeRateLimited is the server's `errors[].code` on every rate-limit refusal, from the per-surface request-rate plug and from a limit raised inside an action alike. The status is always 429, and the class is decided on the status: a 429 that reaches the CLI without the code (an intermediary's, say) is still a rate limit. The code is what a caller that inspects the envelope itself should match.
Variables ¶
var ( ErrForbidden = errors.New("forbidden") ErrNotFound = errors.New("not found") ErrBadRequest = errors.New("bad request") ErrRateLimited = errors.New("rate limited") ErrServer = errors.New("server error") )
Class sentinels. An *APIError wraps exactly one of them, so a caller can errors.Is the class while still showing the server's detail text.
Functions ¶
func Do ¶
Do issues an authenticated request and returns the body on 2xx. Any other status is an *APIError wrapping its class sentinel, with the Retry-After header preserved on 429.
func DoRaw ¶
func DoRaw(ctx context.Context, cfg Config, method, path string, body []byte) (*http.Response, []byte, error)
DoRaw is Do without the status classification: the response (its body already read, capped and closed) comes back for a client with its own error envelope to parse, which proof generation's `meta.code` needs. A missing credential is still an *APIError, because no request is sent.
func Send ¶ added in v0.16.0
Send issues req through the shared client, reads its body (capped at httpclient.MaxResponseSize) and closes it. A 429 is repeated once, after the wait the refusal names: the Retry-After header, else `meta.retry_after_ms`, else httpclient.DefaultRetryAfter; never when `meta.retry_after_ms` is null, and not when the wait is over httpclient.MaxRetryAfter. The wait is jittered (httpclient.Jitter): the server's windows are aligned to the clock minute, and a client that retried on the exact second it was told would arrive at the boundary with every other refused client. Whatever the second attempt answers is returned as is. Callers that build their own *http.Request (`auth status`'s probes, `verify --remote`) go through Send too, so the policy is one. A transport failure is returned unwrapped.
func SetPageQuery ¶
SetPageQuery writes the page parameters every keyset-paged list shares: page[limit] for the page size, page[after] or page[before] to continue from a cursor in either direction, and page[count]=true when the caller wants the server's total. Sort is the caller's, and must be re-sent on every page: this client rebuilds the query rather than following the server's links, so nothing carries over.
func SetRateLimitNotifier ¶ added in v0.16.0
SetRateLimitNotifier installs the function Send calls before the one retry of a rate-limited request, with the wait about to be observed and the limit the refusal named (0 when it named none), so a command can tell the holder why it has gone quiet. Nil, the default, tells no one.
Types ¶
type APIError ¶
type APIError struct {
Status int
Code string // errors[].code, the server's machine-readable reason, when present
Pointer string // errors[].source.pointer, when present
Detail string
// RetryAfter is the verbatim Retry-After header on a 429. The
// per-surface request-rate plug sends one (whole seconds, at least
// 1); a refusal raised inside an action does not, and names its wait
// in RetryAfterMS instead.
RetryAfter string
// RetryAfterMS is `meta.retry_after_ms` on a 429: the wait in
// milliseconds before the request may be repeated. Zero when the
// refusal did not carry one (see NeverAdmitted for the null case).
RetryAfterMS int64
// NeverAdmitted is set when `meta.retry_after_ms` was null: the
// request costs more than the limit allows on its own, and no wait
// admits it. Do not retry.
NeverAdmitted bool
// Limit is `meta.limit` on a 429, the limit that refused the request,
// when carried. Its window is server configuration and is not carried,
// so it is shown as sent and never assumed to be "per minute".
Limit int64
// Sentinel is the class this error belongs to; Unwrap returns it. A
// resource package may narrow it to one of its own domain sentinels
// once it has read the structural discriminators, as teams does for
// the plan-limit and entitlement rejections on create.
Sentinel error
}
APIError carries the HTTP status and the preserved `errors[].detail` (falling back to `title`) from the JSON:API error envelope.
func ErrorFromResponse ¶ added in v0.16.0
ErrorFromResponse classifies a non-2xx response whose body has been read: ParseError, plus the Retry-After header a 429 may carry.
func NotFound ¶
NotFound is the error a client returns when a filter-style lookup came back empty and there was no 404 to classify.
func ParseError ¶
ParseError classifies a non-2xx response. It keeps `errors[].detail` (or `title`) and the `source.pointer` of the first error that carries one: the server can return several errors at once (a free-plan user requesting team_retains trips both the plan-limit and the entitlement rejection), and the pointer is the structural discriminator, so it wins over array position. On a 429 it also reads the rate-limit meta: `retry_after_ms` (a number, or null for "never admitted") and `limit`.
func (*APIError) RetryAfterDelay ¶ added in v0.16.0
RetryAfterDelay is the wait the refusal named: the Retry-After header when it carried one, else `meta.retry_after_ms`. ok is false when it named none, or the error is not a 429.
func (*APIError) RetryDelay ¶ added in v0.16.0
RetryDelay reports how long to wait before repeating a rate-limited request, and whether repeating it can help at all. The wait is RetryAfterDelay when the refusal named one, else httpclient.DefaultRetryAfter. It is false when the error is not a 429, when `meta.retry_after_ms` was null (no wait admits the request), and when the wait is over httpclient.MaxRetryAfter; in the last case the delay is still returned so it can be shown.
type Config ¶
type Config struct {
APIURL string // e.g. https://www.truestamp.com/api/json
Team string // optional tenant id; sent verbatim as the `tenant` header
}
Config carries what a request needs beyond the credential, which the process-wide auth.Authorizer installed in cmd/root supplies out of band.
type PageInfo ¶
type PageInfo struct {
// NextCursor is the page[after] value lifted from links.next; empty on
// the last page. An unparseable link also reads as empty, which is the
// safe reading: a bad cursor would otherwise loop.
NextCursor string
// PrevCursor is the page[before] value lifted from links.prev; empty on
// the first page.
PrevCursor string
// Total is meta.page.total, present only when the request asked for
// page[count]=true; zero otherwise.
Total int
// Limit is meta.page.limit, the page size the server actually used.
// Every collection clamps page[limit] to its max_page_size (250 by
// default) rather than refusing it, so this can be smaller than what
// was asked for; zero when the server did not report one.
Limit int
}
PageInfo is what a list response says about the pages around it.