Documentation
¶
Overview ¶
Package client is a typed HTTP client for the Readur server API.
Index ¶
- Constants
- Variables
- func Backoff(minWait, maxWait time.Duration, attemptNumber int, resp *http.Response) time.Duration
- func CheckRetry(ctx context.Context, resp *http.Response, err error) (bool, error)
- func ClassifyStatus(code int, body string) error
- func IsRetryableErr(err error) bool
- func IsRetryableStatus(code int) bool
- func NewRetryableClient() *retryablehttp.Client
- type Client
- func (c *Client) Do(req *http.Request) (*http.Response, error)
- func (c *Client) DoJSON(ctx context.Context, method, url string, body []byte) (*http.Response, error)
- func (c *Client) DoStreaming(ctx context.Context, method, url string, contentType string, ...) (*http.Response, error)
- func (c *Client) ListLabels(ctx context.Context) ([]Label, error)
- func (c *Client) Login(ctx context.Context, req LoginRequest) (*LoginResult, error)
- func (c *Client) ServerURL() string
- func (c *Client) SetDocumentLabels(ctx context.Context, documentID string, labelIDs []string) error
- func (c *Client) URL(path string) string
- func (c *Client) Upload(ctx context.Context, params UploadParams) (*UploadResult, error)
- type Label
- type LoginRequest
- type LoginResult
- type Options
- type ServerResponseSummary
- type UploadParams
- type UploadResult
Constants ¶
const ( MaxRetries = 3 MinBackoff = 1 * time.Second MaxBackoff = 10 * time.Second )
Retry constants enforce the budget from spec.md Q2:
- 4 total attempts (1 initial + 3 retries)
- exponential backoff with jitter, min 1 s, max 10 s between attempts
- on 429, wait max(exponential_backoff, Retry-After)
Variables ¶
var UploadAttempts atomic.Int64
UploadAttempts is incremented each time a body stream is (re)opened. Tests read it to verify retry behavior re-opens the file.
Functions ¶
func Backoff ¶
Backoff selects the duration to wait between attempts. It honors Retry-After on 429 (and, by extension, on 503), taking the maximum of the exponential-backoff-with-jitter value and the server-hinted delay. Jitter is ±25% of the exponential value.
func CheckRetry ¶
CheckRetry is the retryablehttp.CheckRetry implementation for this client. It returns (shouldRetry, err) where a non-nil err halts the request immediately.
We retry when (a) the request's context is still alive, (b) the underlying error is in the retryable transport class, or (c) the HTTP status is in the retryable class (5xx, 408, 429).
func ClassifyStatus ¶
ClassifyStatus converts an HTTP status + optional body into a CLIError with the appropriate exit code. Used after retries are exhausted.
func IsRetryableErr ¶
IsRetryableErr reports whether a transport-level error is transient enough to justify a retry within the per-request budget.
Retryable classes:
- connection reset by peer
- broken pipe
- unexpected EOF mid-response
- DNS temporary errors
- context deadline exceeded (when the user did not cancel)
Non-retryable classes (explicit):
- x509 / TLS verification failures — the posture is intentional; retrying changes nothing
- URL parse errors — a fix is a code change, not time
func IsRetryableStatus ¶
IsRetryableStatus reports whether an HTTP status is in the retryable class per the clarification in spec.md Q2/Q3: 5xx, 408, 429.
func NewRetryableClient ¶
func NewRetryableClient() *retryablehttp.Client
NewRetryableClient returns a *retryablehttp.Client wired with the project's CheckRetry + Backoff + attempt budget. The returned client has Logger disabled by default (the CLI does its own structured logging); callers may override .Logger if needed.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client wraps retryablehttp with bearer injection, TLS posture control, and the mandatory per-request stderr warning when TLS verification is disabled (FR-016).
func (*Client) Do ¶
Do issues a simple (non-body-replayable) request. Used for GETs and POSTs with small in-memory bodies.
func (*Client) DoJSON ¶
func (c *Client) DoJSON(ctx context.Context, method, url string, body []byte) (*http.Response, error)
DoJSON is a convenience wrapper for POSTing a JSON body.
func (*Client) DoStreaming ¶
func (c *Client) DoStreaming( ctx context.Context, method, url string, contentType string, body retryablehttp.ReaderFunc, ) (*http.Response, error)
DoStreaming issues a request whose body is produced by the provided ReaderFunc. The function is called fresh before every attempt so retries see a rewound body. The reader returned must be a fresh stream (e.g. re-open the underlying file).
func (*Client) ListLabels ¶
ListLabels calls GET /api/labels and returns the server's labels. The method tolerates two wire shapes observed in the wild:
- `{"labels": [...]}` — the documented envelope
- `[...]` — a bare array, emitted by some deployments
func (*Client) Login ¶
func (c *Client) Login(ctx context.Context, req LoginRequest) (*LoginResult, error)
Login POSTs the credentials to /api/auth/login and parses the response. The username in the result is taken from the server's echo when present, otherwise from the request — so callers can always trust `.Username`.
Login IS the rotation endpoint: a 401 here means the supplied credentials are wrong, never "token expired". Login therefore disables automatic rotation for its own DoJSON call so a bad password can never trigger a second login attempt.
func (*Client) SetDocumentLabels ¶
SetDocumentLabels replaces the labels attached to a document.
The Readur upload endpoint (POST /api/documents) does not process a "labels" multipart field — label assignment is done with this separate call. Empty labelIDs is a no-op (no request issued), so the upload code path can call this unconditionally without an extra branch on its own.
func (*Client) Upload ¶
func (c *Client) Upload(ctx context.Context, params UploadParams) (*UploadResult, error)
Upload streams the file at params.LocalPath to POST /api/documents as multipart/form-data. Memory is O(1) in file size; the file is re-opened on every retry via retryablehttp.ReaderFunc.
Note on the path: the public Readur docs at docs.readur.app/api-reference/ describe POST /api/documents/upload, but real Readur deployments return 405 on that path (the /upload segment is parsed as a document id). The server's OPTIONS/Allow headers treat POST /api/documents as the create endpoint, and that is what this client uses.
type Label ¶
type Label struct {
ID string `json:"id"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
Description string `json:"description,omitempty"`
DocumentCount int `json:"document_count,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at,omitzero"`
}
Label mirrors one entry in the Readur labels collection. Fields map to the JSON shape documented at docs.readur.app/api-reference/. Unknown fields are tolerated — json decoding ignores extras.
type LoginRequest ¶
LoginRequest is the JSON body for POST /api/auth/login.
type LoginResult ¶
type LoginResult struct {
Token string
Username string
ExpiresAt time.Time // zero if the server did not return one
}
LoginResult is the subset of the server's response the CLI consumes. The server-side shape per docs:
{
"token": "<jwt>",
"user": {"username": "alice", ...},
"expires_at": "2026-05-20T12:00:00Z"
}
type Options ¶
type Options struct {
ServerURL string
Token string
UserAgent string
InsecureSkipVerify bool
// WarnOut receives the per-request TLS warning when insecure mode is
// active. Typically wired to os.Stderr. If nil, warnings are dropped.
WarnOut io.Writer
// ProfileName is embedded in the TLS warning for clarity.
ProfileName string
// Username/Password are the saved credentials used by automatic
// token rotation. Both must be non-empty for rotation to run.
Username string
Password string
// TokenExpiry is the wall-clock expiry of the current Token, as
// obtained at login. Proactive rotation fires when TokenExpiry is
// past or within tokenExpirySkew of now. Zero disables proactive
// rotation.
TokenExpiry time.Time
// OnTokenRotate is invoked after a successful rotation with the
// freshly issued token and its expiry. The CLI wires this to
// persist the new values back into the active profile. nil is
// allowed — rotation still works; the caller just won't see it.
OnTokenRotate func(newToken string, expiresAt time.Time)
}
Options configures a Client. Zero values are sensible.
When Username and Password are both set, the client will attempt to silently re-authenticate: proactively, if TokenExpiry is within the tokenExpirySkew window; reactively, once per request, on HTTP 401. A successful rotation calls OnTokenRotate so the caller (the CLI) can persist the refreshed token back to config.toml. See research.md §11.
type ServerResponseSummary ¶
type ServerResponseSummary struct {
DocumentID string // populated on successful upload (201)
StatusCode int // HTTP status or 0 if the request never reached the server
ErrorBody string // trimmed server error body on non-2xx
Retryable bool // classifier output
}
ServerResponseSummary mirrors the authoritative outcome of a single HTTP call. Purely internal — not persisted.
type UploadParams ¶
type UploadParams struct {
LocalPath string
DisplayName string
Title *string
OCREnabled *bool
Language *string
}
UploadParams are the server-visible fields for a single-file upload.
Labels are intentionally absent: the upstream Readur upload handler (POST /api/documents) ignores any "labels" multipart field, and label assignment is done with a separate PUT /api/labels/documents/{id} call (see Client.SetDocumentLabels). The CLI's runUpload composes the two calls.
type UploadResult ¶
UploadResult is the subset of the server response the CLI consumes.