jsonapi

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

View Source
const CodeInvalidKeyset = "invalid_keyset"

CodeInvalidKeyset is the server's code for a page[after] / page[before] value that is not a cursor it issued.

View Source
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

View Source
var (
	ErrUnauthorized = errors.New("not authenticated")
	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

func Do(ctx context.Context, cfg Config, method, path string, body []byte) ([]byte, error)

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 Get

func Get(ctx context.Context, cfg Config, path string) ([]byte, error)

Get issues an authenticated GET and returns the body on 2xx.

func Post

func Post(ctx context.Context, cfg Config, path string, payload any) ([]byte, error)

Post marshals payload as the request body and POSTs it.

func Send added in v0.16.0

func Send(req *http.Request) (*http.Response, []byte, error)

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

func SetPageQuery(q url.Values, limit int, after, before string, count bool)

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

func SetRateLimitNotifier(fn func(wait time.Duration, limit int64))

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.

func SortByID

func SortByID(oldestFirst bool) string

SortByID is the one ordering every list uses: by id, which for the ULIDs and UUIDv7s these resources carry is insertion time. Newest first unless the caller asked to start from the beginning.

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

func ErrorFromResponse(resp *http.Response, body []byte) *APIError

ErrorFromResponse classifies a non-2xx response whose body has been read: ParseError, plus the Retry-After header a 429 may carry.

func NotFound

func NotFound(detail string) *APIError

NotFound is the error a client returns when a filter-style lookup came back empty and there was no 404 to classify.

func ParseError

func ParseError(status int, body []byte) *APIError

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) Error

func (e *APIError) Error() string

func (*APIError) RetryAfterDelay added in v0.16.0

func (e *APIError) RetryAfterDelay() (time.Duration, bool)

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

func (e *APIError) RetryDelay() (time.Duration, bool)

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.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

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.

func ParsePage

func ParsePage(body []byte) PageInfo

ParsePage reads links.next, links.prev and meta.page.total from a list body. It tolerates any of them being absent.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL