client

import infraclient "github.com/infrapi/lib/pkg/client"

The client of the APIs InfraPI services expose with server: a resty client with InfraPI defaults - bearer token, JSON headers, TLS 1.2 floor, retry on transient failures and optional hedging - that builds the paths of api.

Creating a client#

compute := api.Group{Name: "compute", Domain: "giservices.io", Version: "v1"}

client, err := infraclient.NewClient(&infraclient.ClientOptions{
	Server:    "compute.example.com",
	API:       compute,
	Namespace: "core",
	Token:     token,
})

Server and API are mandatory, and API must pass api.Group.Validate; everything else has a default:

OptionTypeDefault
Protocolstringhttps
Portint443
Namespacestringnone: each request must then name one
UserAgentstringinfrapi-lib-client-{name}.{domain}
Headersmap[string]stringContent-Type and Accept set to application/json
GlobalTimeoutint (seconds)60
RetryCountint3
RetryWaitTimetime.Duration1s
RetryMaxWaitTimetime.Duration30s

Token, when set, is sent as a bearer token on every request.

Update(opts) rebuilds an existing client in place with a new set of options, which is what NewClient calls internally.

Request URL#

Requests name a resource, and the client builds:

{Protocol}://{Server}:{Port}/apis/{name}.{domain}/{version}/namespaces/{namespace}/{resource}

So on the client above, client.Get("vm/123", nil) requests https://compute.example.com:443/apis/compute.giservices.io/v1/namespaces/core/vm/123, and client.Post("vm/123/reboot", nil) its reboot action. Each segment of the resource is escaped. A resource with an empty, . or .. segment, or a request without a namespace, fails before anything is sent.

Sending requests#

resp, err := client.Post("vm", &infraclient.RequestOptions{
	Data:      body,                                  // raw request body
	Headers:   map[string]string{"X-Request-Id": id}, // merged into the request
	Server:    "compute-eu.example.com",              // per-request host override
	Namespace: "edge",                                // per-request namespace override
})

Get, Post, Put, Patch, Delete and Head all delegate to Do(method, path, opts), and opts may be nil.

type Response struct {
	Code int
	Body []byte
}

Response is always returned, even alongside an error, so the status code and body of a failed call stay readable. When the transport itself fails, Code is 500.

Success range#

A response is an error when its status code is below MinHttpCode or above MaxHttpCode. The defaults are 200 and 400, and both bounds are inclusive, so a 400 Bad Request counts as success unless the range is narrowed:

resp, err := client.Get("vm", &infraclient.RequestOptions{
	MinHttpCode: 200,
	MaxHttpCode: 299,
})

The error message is the HTTP status text; inspect resp.Body for the server payload.

Retry#

Retries are on by default and trigger on a transport error, 429 Too Many Requests, or any 5xx, with exponential backoff between RetryWaitTime and RetryMaxWaitTime.

client, err := infraclient.NewClient(&infraclient.ClientOptions{
	Server:           "compute.example.com",
	API:              compute,
	RetryCount:       5,
	RetryWaitTime:    200 * time.Millisecond,
	RetryMaxWaitTime: 5 * time.Second,
})

Hedging#

Hedging fires staggered concurrent requests and keeps the first success, trading extra load for a shorter tail latency.

client, err := infraclient.NewClient(&infraclient.ClientOptions{
	Server: "compute.example.com",
	API:    compute,
	Hedging: &infraclient.HedgingOptions{
		Enabled:      true,
		Delay:        150 * time.Millisecond,
		UpTo:         2,
		MaxPerSecond: 5,
	},
})
FieldMeaning
EnabledTurns hedging on.
DelayWait before each additional request. 0 sends them immediately.
UpToMaximum concurrent hedged requests. Anything below 2 leaves hedging inert.
MaxPerSecondRate cap across hedged requests, fractional allowed. 0 is unlimited.
NonReadOnlyAllowedAlso hedge POST, PUT, PATCH and DELETE.

Warning

Hedging duplicates requests. It is limited to GET, HEAD, OPTIONS and TRACE unless NonReadOnlyAllowed is set, and resty disables retry while hedging is on.

TLS and mTLS#

The client always negotiates TLS 1.2 or later. Certificates can be passed as PEM bytes or as filesystem paths; when both are given, the in-memory content wins.

client, err := infraclient.NewClient(&infraclient.ClientOptions{
	Server: "compute.example.com",
	API:    compute,

	CACertPath: "/etc/ssl/certs/internal-ca.pem",

	ClientCertPath: "/etc/ssl/certs/myapp.pem",
	ClientKeyPath:  "/etc/ssl/private/myapp.key",
})
OptionPurpose
CACertContent / CACertPathTrust an internal CA for the server certificate.
ClientCertContent / ClientKeyContentmTLS keypair from memory.
ClientCertPath / ClientKeyPathmTLS keypair from disk.

An mTLS keypair needs both halves: cert and key must come from the same source.

Routes outside the layout#

Do always builds a namespaced path. A route of the same service outside it - a cluster-wide endpoint such as auth’s /apis/authorization.giservices.io/v1/check, or one under /-/ - goes through NewRequest, which keeps this client’s TLS, retry, header and timeout configuration without exposing resty to the caller:

resp, err := client.NewRequest(ctx).Get("https://compute.example.com/-/metadata")

NewRequest(ctx) returns a *Request with Get, Post, Put, Patch and Delete, each taking the full URL and returning the same *Response as Do. Unlike Do, a Request applies no success-range check: any response that comes back, 2xx or not, is returned without an error, and the caller decides what counts as a failure.

StatusText(code) and the StatusBadGateway constant are net/http’s own, re-exported so a caller deciding what counts as a failure - and building an errors.Error to report it - never needs to import net/http itself either:

if resp.Code < 200 || resp.Code >= 300 {
	return infraerrors.New(resp.Code, "myapp.Call", "%s", infraclient.StatusText(resp.Code))
}

Escape hatch#

client.RestyClient is the underlying *resty.Client. Anything neither wrapper exposes - middlewares, cookies, proxies, custom transports - is configurable there.

examples/client runs all of this against a local server, including retry and hedging.