Clientkit is the outbound-client shell for Go services. It keeps HTTP and TCP usage recognizable while adding immutable client identity, bounded execution policy, safe retries, timeouts, propagation, observability, cached health, and readiness integration.
Clientkit is independently useful. It integrates with the rest of the Kit Series through Opskit, but it does not require Servekit, Workerkit, Configkit, or Dependkit.
| Clientkit owns | The application owns |
|---|---|
| Stable outbound-client identity | Endpoint and credential configuration |
| HTTP execution, retry, and timeout policy | Whether repeating an operation is semantically safe |
| TCP connection establishment and optional TLS | Protocol exchanges over a returned net.Conn |
| Trace propagation and observer events | OpenTelemetry SDK, exporter, and provider lifecycle |
| Cached client health and readiness projection | Scheduling active checks |
| Safe bounded outcome and failure classifications | Closing returned HTTP bodies and TCP connections |
Clientkit is not service discovery, client-side load balancing, a circuit breaker, an authentication framework, a generated SDK system, or a raw TCP connection pool. Dependkit remains the generic external-dependency health package; Clientkit does not require it.
go get github.com/jaredjakacky/clientkit@latestImport only the packages needed by the application. The root package depends only on Opskit outside the standard library. OpenTelemetry API dependencies are kept in the relevant protocol and adapter packages; Clientkit never initializes an SDK or exporter.
package main
import (
"context"
"io"
"log"
"net/http"
"github.com/jaredjakacky/clientkit"
"github.com/jaredjakacky/clientkit/httpclient"
)
func main() {
client, err := httpclient.New(httpclient.Config{
Config: clientkit.Config{Name: "payments"},
BaseURL: "https://payments.example/api/",
})
if err != nil {
log.Fatal(err)
}
request, err := client.NewRequest(context.Background(), http.MethodGet, "status", nil)
if err != nil {
log.Fatal(err)
}
response, err := client.Do(request)
if err != nil {
log.Fatal(err)
}
defer response.Body.Close()
_, _ = io.Copy(io.Discard, response.Body)
}httpclient.New validates configuration without performing network I/O.
Do preserves ordinary net/http response/error semantics: a response
rejected by Clientkit's classifier is still returned with a nil error. The
caller owns every returned response body and must close it.
client, err := tcpclient.New(tcpclient.Config{
Config: clientkit.Config{Name: "events"},
Address: "events.example:443",
TLS: tcpclient.TLSConfig{Enabled: true},
})
if err != nil {
log.Fatal(err)
}
conn, err := client.Dial(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()With TLS enabled and no custom tls.Config, Clientkit verifies certificates,
infers the verification name from the configured address, and requires TLS 1.2
or newer. A successful connection is an ordinary caller-owned net.Conn.
Clientkit does not retain it or place it in a hidden pool.
| Area | Default |
|---|---|
| Readiness policy | Optional |
| Health checks | Disabled; cached health begins unknown |
| HTTP response policy | Accept 2xx |
| HTTP total timeout | 30 seconds |
| HTTP attempt timeout | 10 seconds |
| HTTP retries | Up to 3 attempts for selected idempotent methods and retryable failures |
| HTTP origin policy | Cross-origin requests and host overrides rejected |
| HTTP transport | Bounded production transport with HTTP/2 attempts enabled |
| TCP dial timeout | 5 seconds |
| TCP keepalive | 30 seconds with the built-in dialer |
| TCP security | Plaintext unless TLS is explicitly enabled |
| TLS handshake timeout | 10 seconds |
| Default TLS minimum | TLS 1.2 when Clientkit creates the TLS policy |
Zero often selects a documented production default. Explicit disable fields remove the corresponding Clientkit layer, while parent contexts and caller-owned client behavior remain authoritative. Custom retry and TLS configurations are complete policies rather than partial merges. Consult the API map and Go documentation before overriding defaults.
| Package | Responsibility |
|---|---|
clientkit |
Transport-neutral identity, health, failure classification, observers, registry, and Opskit contracts |
httpclient |
HTTP request construction, execution, retries, timeouts, health checks, and results |
tcpclient |
Raw TCP connection establishment, optional TLS, probes, health checks, and results |
clientkit/otel |
Logical-operation, direct-remote, retry, and health OpenTelemetry adapter |
httpclient/otel |
Per-RoundTrip CLIENT spans, propagation, and optional standard HTTP metrics |
slogobserver |
Safe structured logging adapter |
Packages under internal/ are implementation details and must not be imported.
| Method | Use it when |
|---|---|
Do |
Ordinary net/http response/error semantics are enough |
Execute |
The caller needs Clientkit Result, Outcome, attempts, and FailureClass |
ExecuteWithOptions |
One operation needs an explicit name, classifier, retry policy, retry-safety assertion, or timeout override |
Outcome answers what happened to the logical operation. FailureClass
provides a stable, bounded classification suitable for policy and telemetry.
Result.Err remains the original caller-visible Go error. Response rejection
is policy information and does not manufacture a transport error.
An HTTP retry occurs only when all three independent gates allow it:
- The selected retry policy permits the method and failure or status.
RetrySafetysays repeating the operation is semantically safe.- A request body is absent or mechanically replayable through
Request.GetBody.
The default policy does not blindly retry POST, PATCH, CONNECT, or custom
methods. Authorizing a POST with RetrySafetyIdempotent is an application
assertion. Clientkit does not create or validate idempotency keys, and a retry
after a timeout can duplicate a side effect.
For eligible operations, the default retries connection refused, reset, closed,
temporary or unknown DNS, and otherwise unclassified transport failures. It
fails immediately for recognized TLS failures, DNS not-found, and a
RoundTripper that returns neither a response nor an error.
TransportRetryNone and TransportRetryAll provide explicit narrower and
broader behavior; timeouts remain controlled separately.
RetrySafety also governs method-preserving 307 and 308 redirects. The default
follows them only for Clientkit's built-in idempotent methods,
RetrySafetyNever rejects them, and RetrySafetyIdempotent permits them.
Ordinary 301, 302, and 303 redirect behavior remains unchanged. A non-empty
body still requires Request.GetBody before net/http can follow a 307 or 308.
http.NewRequest and http.NewRequestWithContext populate GetBody for common
in-memory readers such as bytes.Buffer, bytes.Reader, and strings.Reader.
ExecuteWithOptions takes ownership of a non-nil request body and closes it,
including when validation prevents the first attempt.
See Usage for complete retry and body rules.
The total timeout spans attempts, retry delays, and final response-body use.
The attempt timeout restarts for each Clientkit attempt and also remains active
for the final body. A parent context, http.Client.Timeout, or transport limit
may end work earlier.
Logical and physical observations finish when final response headers or a terminal error are available; they do not measure body consumption. Timeout cleanup remains attached to the final body until EOF, body error, close, or context completion. Always read or close the body promptly. If every timeout is disabled and the parent has no deadline, abandoning the body can retain resources indefinitely.
NewRequest uses normal RFC 3986 reference resolution. The BaseURL path is a
convenient base, not a confinement boundary: root-relative and parent references
can replace or escape it. Absolute references and fragments are rejected.
Execution rejects changes to scheme, host, or effective port by default, as
well as Request.Host overrides. Enabling cross-origin execution may forward
caller-supplied headers or permit an HTTPS downgrade and should be paired with a
restrictive redirect policy.
A caller-supplied *http.Client remains caller-owned and is never mutated or
retained. Clientkit shallow-copies its top-level value during construction, so
later field assignments do not change Clientkit behavior. Referenced transports,
jars, and callback state remain shared. Calling CloseIdleConnections may
affect other users when the construction-time transport is shared.
Protocol checks are disabled by default. Check and Registry.CheckAll are the
active operations that may contact dependencies. Health, Snapshot,
Status, Readiness, and Inspect only project cached state and never perform
synchronous dependency I/O.
Registered clients own ordinary cached health. When Registry.CheckAll must
synthesize a client-specific failure that the client cannot cache, Registry
passive projections retain that exceptional result until a later client-owned
assessment supersedes it.
Clientkit creates no scheduler or background goroutine. Applications may run
checks directly or use Workerkit to periodically execute the Registry's Opskit
CheckGroup. Servekit can then present the same cached state through Opskit.
See Health and readiness and
Composition.
When Clientkit owns the default HTTP client and no observer is supplied, it installs the complete default HTTP model:
- One logical Clientkit HTTP operation represented by an INTERNAL span.
- One CLIENT span for every instrumented
RoundTripinvocation, including retries and redirects. - Trace-context injection from the corresponding
RoundTripspan. - Low-cardinality Clientkit operation, attempt, retry, and health metrics.
A caller-owned HTTP client or a non-nil custom observer replaces part of that
automatic boundary. Use httpclient/otel.NewTransport and
clientkit/otel.New explicitly when those cases still need complete tracing.
Standard HTTP duration metrics and request-target span attributes are opt-in because they introduce server identity. Raw errors are excluded by default from OpenTelemetry and slog because they may contain URLs, hosts, certificate text, or application data. Applications own SDK/exporter lifecycle and must configure global providers and propagation before constructing Clientkit adapters.
See Observability and Operational safety.
HTTP and TCP clients
│
Clientkit Registry
│
Opskit
├── Workerkit periodically refreshes checks
└── Servekit presents passive status/readiness/inspection
Clientkit's per-client readiness policy is separate from the policy used to register the Clientkit Registry with Opskit. Register the Registry as required when its client policies should gate service readiness. If Workerkit only schedules those checks, disable that worker's independent readiness contribution to avoid two gates for the same state.
- Getting started
- Usage
- Health and readiness
- Observability
- Operational safety
- Kit Series composition
- API map
- Examples
- Go package documentation
Runnable examples live under examples/. They use local
servers and listeners and require no external service or credentials.
go run ./examples/http-basic
go -C examples/kit-series-composition run .make help
make verify
make test-race
make govulncheckmake verify checks formatting, the root dependency boundary, vet, tests,
runnable examples, and tidy module state.
Report security issues using SECURITY.md. Use the repository's issue tracker for ordinary defects and proposals.
Releases are created through the manual GitHub Actions release workflow. The workflow validates the requested semantic version and target commit, runs the release verification gate, and creates the tag and GitHub Release only after those checks pass. Do not manually push a release tag around that gate.
Clientkit intentionally keeps its scope narrow. New transports or policy systems should be added only when a concrete outbound-client requirement cannot be expressed through ordinary Go and the existing package boundaries.
Clientkit is licensed under the terms in LICENSE.