Documentation
¶
Overview ¶
Package ghkit bundles ETag caching, rate limiting, retry on transient failures, and a proactive token bucket behind a single options-pattern API. New is generic over the returned client type, so ghkit has no compile-time dependency on any specific GitHub SDK; pass any func(*http.Client) T factory at the call site (canonically github.com/google/go-github's NewClient).
Transport stack (outer -> inner, each layer optional):
http.Client
UserAgent (overwrites User-Agent) [WithUserAgent]
Throttle (x/time/rate proactive) [WithRequestsPerSecond]
RateLimit (go-github-ratelimit v2) [default ON]
Retry (5xx + transient net errors) [WithRetry]
oauth2.Transport (clones req, sets Auth) [WithToken/WithTokenSource]
ETag (hashes auth'd clone) [WithETagCache]
Base (*http.Transport,
DisableCompression=true) [WithBaseTransport]
Retry sits below RateLimit so 429s are deferred to the rate-limit layer; sits above oauth2 so retried requests get the latest token via oauth2's per-call Source.Token().
The ETag precompute algorithm is the reason to use this kit. GitHub's server-side ETag hash includes the Authorization header, so a passive store-and-forward cache falls over under rotating auth (GitHub App installation tokens refresh hourly). The etag sub-package reproduces that hash client-side so cached entries stay useful across rotations. Algorithm credit: https://github.com/bored-engineer/github-conditional-http-transport
Auth patterns ¶
ghkit offers two auth paths. Pick one; do not combine them.
ghkit owns auth: pass WithToken or WithTokenSource. ghkit inserts an oauth2.Transport into the stack and injects Authorization on every outbound request. Works for static PATs and for oauth2.TokenSource implementations (e.g. ghinstallation for GitHub App installation tokens).
ghkit is auth-free; the SDK owns auth via per-call cloning. Omit WithToken/WithTokenSource. Build one ghkit HTTPClient at startup, hand it to your SDK, and let the SDK inject the current token per call (e.g. go-github's (*Client).WithAuthToken, which clones the go-github Client above ghkit's shared transport). The ETag LRU and rate-limit bucket persist across token rotation. This is the canonical pattern for Kubernetes operators that reconcile with a per-reconcile installation token.
Sub-packages (etag, ratelimit, throttle) are independently importable for callers composing their own stack.
Example (EtagOnly) ¶
Example_etagOnly uses only the etag sub-package inside a hand-built transport chain.
package main
import (
"fmt"
"net/http"
"github.com/pcanilho/go-github-kit/etag"
)
func main() {
rt, err := etag.NewTransport(nil,
etag.WithCache(etag.NewLRUCache(1024)),
etag.WithKeyScope("tenant-42"),
)
if err != nil {
fmt.Println("construct:", err)
return
}
hc := &http.Client{Transport: rt}
resp, err := hc.Get("https://api.github.com/meta")
if err != nil {
fmt.Println("get:", err)
return
}
if err := resp.Body.Close(); err != nil {
fmt.Println("close:", err)
}
}
Output:
Example (Throttle) ¶
Example_throttle wraps any http.RoundTripper in a token-bucket cap.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/pcanilho/go-github-kit/throttle"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}))
defer srv.Close()
rt, err := throttle.NewTransport(http.DefaultTransport, 10.0, throttle.WithBurst(1))
if err != nil {
fmt.Println("construct:", err)
return
}
hc := &http.Client{Transport: rt}
resp, err := hc.Get(srv.URL)
if err != nil {
fmt.Println("get:", err)
return
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
Output: 200
Index ¶
- Variables
- func HTTPClient(opts ...Option) (*http.Client, error)
- func New[T any](factory func(*http.Client) T, opts ...Option) (T, error)
- type Option
- func WithBaseTransport(rt http.RoundTripper) Option
- func WithETagCache(opts ...etag.Option) Option
- func WithLogger(l *slog.Logger) Option
- func WithRateLimit(opts ...ratelimit.Option) Option
- func WithRateLimitDisabled() Option
- func WithRequestsPerSecond(rps float64, burst int) Option
- func WithRetry(opts ...retry.Option) Option
- func WithTimeout(d time.Duration) Option
- func WithToken(pat string) Option
- func WithTokenSource(src oauth2.TokenSource) Option
- func WithUserAgent(ua string) Option
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrConflictingAuth = errors.New("ghkit: WithToken and WithTokenSource are mutually exclusive") ErrConflictingRateLimit = errors.New("ghkit: WithRateLimit and WithRateLimitDisabled are mutually exclusive") ErrPreAuthedBaseWithAuth = errors.New("ghkit: WithBaseTransport with a non-*http.Transport base cannot be combined with WithToken or WithTokenSource") ErrNonPositiveRPS = errors.New("ghkit: WithRequestsPerSecond requires rps > 0 and burst >= 1") ErrNilFactory = errors.New("ghkit: New requires a non-nil factory function") )
Sentinel errors for config validation. Callers can use errors.Is to distinguish specific failure modes in tests or runtime handling.
Functions ¶
func HTTPClient ¶
HTTPClient builds an *http.Client with the configured transport stack. Returns an error when the option combination is invalid; see the sentinel errors above.
Example ¶
ExampleHTTPClient is the library-agnostic entry point: a configured *http.Client you can hand to any client library that takes one.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
ghkit "github.com/pcanilho/go-github-kit"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}))
defer srv.Close()
hc, err := ghkit.HTTPClient(
ghkit.WithToken("fake-token"),
ghkit.WithETagCache(),
)
if err != nil {
fmt.Println("construct:", err)
return
}
resp, err := hc.Get(srv.URL)
if err != nil {
fmt.Println("get:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Print(string(body))
}
Output: ok
func New ¶
New builds an *http.Client via HTTPClient and plumbs it into the caller-supplied factory. Generic over the returned type so ghkit has no compile-time dependency on any specific GitHub SDK; pass whichever constructor you use at the call site.
Canonical usage:
import "github.com/google/go-github/v85/github"
gh, err := ghkit.New(github.NewClient,
ghkit.WithToken(os.Getenv("GITHUB_TOKEN")),
ghkit.WithETagCache(),
)
Custom construction (UserAgent, GitHub Enterprise BaseURL, any other post-construction tweaks) goes inside a factory closure:
gh, err := ghkit.New(func(hc *http.Client) *github.Client {
c := github.NewClient(hc)
c.UserAgent = "my-app/1.0"
return c
}, opts...)
For GitHub Enterprise, call github.NewClient(hc).WithEnterpriseURLs(base, upload) inside the closure. The base URL must end with a trailing slash; go-github returns an error if it does not.
When factory is nil, New returns the zero value of T and ErrNilFactory. When HTTPClient returns an error (invalid option combination), New proxies the error.
Types ¶
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures a Transport. The interface form (rather than a bare `func(*config)`) lets us evolve the API without breaking callers.
func WithBaseTransport ¶
func WithBaseTransport(rt http.RoundTripper) Option
WithBaseTransport supplies the bottom of the transport stack. When omitted, a cloned http.DefaultTransport with DisableCompression=true is used. Passing a non-nil RoundTripper that is not an *http.Transport is rejected when ETag caching is enabled (the gzip invariant cannot be enforced on an arbitrary wrapper). Passing nil is equivalent to omitting the option.
DO NOT combine WithBaseTransport with WithToken or WithTokenSource when the supplied transport is not a bare *http.Transport; two auth sources produce undefined winner.
func WithETagCache ¶
WithETagCache enables the precompute-mode ETag cache. Sub-options (etag.WithCache, etag.WithKeyScope, etc.) configure the cache backend and scope.
func WithLogger ¶
WithLogger supplies the slog.Logger used for diagnostic events.
The library is silent by default: omit this option (or pass nil) and no log records are emitted. When set, the supplied logger is forwarded to etag, ratelimit, and retry sub-packages as their default; per-sub-package WithLogger options inside WithRetry/WithETagCache/WithRateLimit can still override.
func WithRateLimit ¶
WithRateLimit configures the reactive rate limiter (go-github-ratelimit). The rate limiter is ENABLED by default; call this only to register callbacks or tune sleep limits.
func WithRateLimitDisabled ¶
func WithRateLimitDisabled() Option
WithRateLimitDisabled turns off the reactive rate limiter. Mutually exclusive with WithRateLimit; combining the two surfaces ErrConflictingRateLimit at construction.
func WithRequestsPerSecond ¶
WithRequestsPerSecond enables the proactive token-bucket throttle. rps <= 0 or burst < 1 returns an error at construction time.
func WithRetry ¶ added in v1.1.0
WithRetry enables the retry middleware. Sub-options (retry.WithMaxAttempts, retry.WithBackoff, retry.WithRetryOn, retry.WithLogger) configure the policy. The default predicate retries idempotent methods on 5xx and recognised transient network errors; 429 is hard-excluded so the rate limiter above owns it.
Retry sits between RateLimit and oauth2 in the chain: 429s never reach retry, and retried requests get the latest token via oauth2's per-call Source.Token().
Each retry attempt consumes a throttle token if WithRequestsPerSecond is in use. A worst-case failing request can briefly use maxAttempts times the nominal RPS budget.
func WithTimeout ¶
WithTimeout sets http.Client.Timeout on the returned client.
func WithToken ¶
WithToken configures static Personal Access Token authentication. Exactly one of WithToken or WithTokenSource may be set.
func WithTokenSource ¶
func WithTokenSource(src oauth2.TokenSource) Option
WithTokenSource configures auth via an oauth2.TokenSource. Use this for GitHub App installation tokens (via ghinstallation or similar) and any other rotating-token setup. Exactly one of WithToken or WithTokenSource may be set.
func WithUserAgent ¶
WithUserAgent sets the User-Agent header on every outbound request at the transport level. Applied after any SDK sets its own User-Agent, so the caller's value wins. User-Agent is not in GitHub's server-side ETag hash domain, so setting this does not interfere with the ETag cache.
An empty string is a no-op: the middleware is not inserted. To suppress User-Agent entirely, supply a base RoundTripper that sets an empty header.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package etag implements GitHub's reverse-engineered ETag algorithm and a conditional-request HTTP transport that uses it.
|
Package etag implements GitHub's reverse-engineered ETag algorithm and a conditional-request HTTP transport that uses it. |
|
Package ghtest provides minimal test helpers for code that uses ghkit.
|
Package ghtest provides minimal test helpers for code that uses ghkit. |
|
Package ratelimit is a thin facade over github.com/gofri/go-github-ratelimit/v2.
|
Package ratelimit is a thin facade over github.com/gofri/go-github-ratelimit/v2. |
|
Package retry wraps an http.RoundTripper with retries on transient failures (5xx responses, network errors, transport-level deadline exceeded).
|
Package retry wraps an http.RoundTripper with retries on transient failures (5xx responses, network errors, transport-level deadline exceeded). |
|
Package throttle wraps an http.RoundTripper with a client-side token-bucket rate limiter backed by golang.org/x/time/rate.
|
Package throttle wraps an http.RoundTripper with a client-side token-bucket rate limiter backed by golang.org/x/time/rate. |