Documentation
¶
Overview ¶
Package transport provides a unified HTTP transport layer for the git-platform-sdk. It is the single entry point through which all platform implementations send requests, so cross-cutting concerns (authentication header injection, retry/backoff, request/response hooks, structured logging, body capture for retry) are implemented in exactly one place.
The package is designed to work in two ways:
Direct usage via Client. Higher-level callers build a Request and pass it to Client.Do, Client.DoJSON, or Client.DoRaw. The response body is captured for the caller to consume.
Transparent integration with third-party SDKs via RoundTripper. The RoundTripper returned by Client.RoundTripper is a standard http.RoundTripper that performs auth/retry/hooks on every request made by any client that builds on net/http. This is how go-github, gitlab client-go, gitea-sdk, forgejo-sdk and go-gitcode are wired up so they benefit from the same cross-cutting behavior without code duplication.
Index ¶
- Constants
- Variables
- func NewStatusError(method, path string, status int, body []byte) error
- type AuthFunc
- type AuthStrategy
- type BearerToken
- type Client
- func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
- func (c *Client) DoJSON(ctx context.Context, req *Request) (*Response, error)
- func (c *Client) DoRaw(ctx context.Context, req *Request) ([]byte, error)
- func (c *Client) NewRetryingRoundTripper() http.RoundTripper
- func (c *Client) RoundTripper() http.RoundTripper
- type Error
- type Hooks
- type Logger
- type LoggerFunc
- type None
- type PrivateToken
- type RateLimiter
- type RateLimiterOption
- type Request
- type RequestHook
- type Response
- type ResponseHook
- type RetryConfig
- type SlogLogger
- type StaticAuth
- type TokenHeader
Constants ¶
const DefaultMaxBodySize = 10 * 1024 * 1024
DefaultMaxBodySize is the default maximum response body size (10 MB).
const DefaultTimeout = 30 * time.Second
DefaultTimeout is the per-request HTTP timeout applied when the caller does not specify one explicitly.
Variables ¶
var ErrEmptyResponse = fmt.Errorf("transport: empty response body")
ErrEmptyResponse is returned by DoJSON when the server returned a 2xx with no body and the caller asked for a non-nil result.
Functions ¶
Types ¶
type AuthStrategy ¶
AuthStrategy applies authentication credentials to an outbound request. Implementations must be safe to call concurrently from multiple goroutines.
type BearerToken ¶
type BearerToken struct{ Token string }
BearerToken sets the standard "Authorization: Bearer <token>" header.
func (BearerToken) Apply ¶
func (b BearerToken) Apply(req *http.Request)
Apply implements AuthStrategy.
type Client ¶
type Client struct {
// BaseURL is the API root used to resolve Request.Path. It is trimmed of
// trailing slashes on construction.
BaseURL string
// Auth is applied to every request. May be nil/None for unauthenticated
// calls.
Auth AuthStrategy
// Retry controls exponential-backoff retry on transient failures. nil
// disables retry entirely.
Retry *RetryConfig
// Hooks receives every request/response for observability. nil is fine.
Hooks *Hooks
// Logger receives structured log entries. nil falls back to noopLogger.
Logger Logger
// Timeout is the per-request timeout applied when ctx has no deadline. A
// non-positive value falls back to DefaultTimeout.
Timeout time.Duration
// Transport is the underlying http.RoundTripper. nil falls back to
// http.DefaultTransport.
Transport http.RoundTripper
// Limiter provides proactive rate limiting. nil disables rate limiting.
Limiter *RateLimiter
// MaxBodySize limits the response body size in bytes. 0 means no limit.
// A value of -1 uses the default limit (10 MB). This prevents OOM from
// malicious or misconfigured servers.
MaxBodySize int64
}
Client is the unified transport. It is safe for concurrent use after construction; do not mutate it after handing it out.
func NewClient ¶
func NewClient(baseURL string, auth AuthStrategy) *Client
NewClient builds a Client with the given base URL and auth strategy. It is the only constructor that is expected to be used in production code; the zero value works but skips the baseURL trimming.
func NewClientWithTransport ¶
func NewClientWithTransport(baseURL string, auth AuthStrategy, rt http.RoundTripper) *Client
NewClientWithTransport builds a Client with a custom underlying transport. Useful for tests with httptest.Server backed transport, or for callers that need to inject TLS / connection-pool tuning.
func (*Client) Do ¶
Do executes req and returns the captured response. It does NOT decode JSON into req.Result; use DoJSON for that. Use Do to inspect the raw body or status code manually.
func (*Client) DoJSON ¶
DoJSON executes req and, when req.Result is non-nil and the response has a body, decodes the body as JSON into req.Result. It returns ErrEmptyResponse when the response has no body and req.Result is non-nil.
func (*Client) DoRaw ¶
DoRaw executes req and returns the response body as raw bytes. It is a convenience over Do for endpoints that return non-JSON payloads (archives, tarballs, plain text, ...).
func (*Client) NewRetryingRoundTripper ¶
func (c *Client) NewRetryingRoundTripper() http.RoundTripper
NewRetryingRoundTripper wraps rt in retry/backoff. The returned RoundTripper can be plugged into any http.Client; retries fire on 429, 5xx, and the configured retry list, but only for idempotent methods {GET, HEAD, PUT, DELETE, OPTIONS} — non-idempotent requests (POST, PATCH, ...) are retried exclusively on errors proving the request never reached the network, unless RetryConfig.RetryWrite is set.
func (*Client) RoundTripper ¶
func (c *Client) RoundTripper() http.RoundTripper
RoundTripper exposes the auth/hooks/logging of this Client as a standard http.RoundTripper so third-party SDK clients built on net/http benefit from the same pipeline. It does NOT apply retries; retries are request-scoped and belong to the call site (e.g. Client.Do). Use NewRetryingRoundTripper to wrap this with retry, if needed.
type Error ¶
type Error struct {
Method string
Path string
Body []byte
Cause error
// contains filtered or unexported fields
}
Error is the structured error returned by Client when a request completes with a non-2xx status code, or when the underlying transport fails. It implements errors.Is / errors.As so callers can branch on either the status class (via IsStatusClass) or the wrapped cause.
The status code is exposed through the StatusCode() method rather than an exported field so that *Error satisfies the statusCoder interface (StatusCode() int) consumed by provider.Wrap — that interface check is what removes the reflection-based fallback there. A struct cannot have a field and a method with the same name, hence the unexported storage field.
func (*Error) Is ¶
Is implements errors.Is. Two *Error values match when they share the same status code, which is the most useful predicate for callers that branch on error class. Identity comparison still works via errors.Is's default path (same pointer).
func (*Error) IsStatusClass ¶
IsStatusClass reports whether the error is in the given status class. For example IsStatusClass(http.StatusInternalServerError) reports 5xx.
func (*Error) StatusCode ¶
StatusCode returns the HTTP status code of the failed response. It makes *Error satisfy the statusCoder-style interface (StatusCode() int) used by provider.Wrap, so the fast interface path matches *transport.Error directly and no reflection fallback is needed.
type Hooks ¶
type Hooks struct {
Request []RequestHook
Response []ResponseHook
}
Hooks is an ordered collection of request and response hooks. The zero value is usable; nil receivers are no-ops.
func (*Hooks) AddRequest ¶
func (h *Hooks) AddRequest(hook RequestHook)
AddRequest appends a request hook.
func (*Hooks) AddResponse ¶
func (h *Hooks) AddResponse(hook ResponseHook)
AddResponse appends a response hook.
func (*Hooks) ExecuteRequest ¶
ExecuteRequest runs every registered request hook in order. The first non-nil error short-circuits. A nil *Hooks is a no-op.
type Logger ¶
type Logger interface {
Debug(msg string, keysAndValues ...any)
Info(msg string, keysAndValues ...any)
Warn(msg string, keysAndValues ...any)
Error(msg string, keysAndValues ...any)
}
Logger is a minimal logging interface compatible with log/slog, zap, zerolog, logrus, and most other Go logging libraries.
Implementations should be safe to call concurrently. The transport layer emits a small set of fixed events: "request ok", "request error", "request failed", "retry: network error", "retry: retryable status", and "roundtrip ok/failed".
func NewDebugLogger ¶
func NewDebugLogger() Logger
NewDebugLogger returns a Logger that writes to stderr at debug level. It checks the GIT_PLATFORM_SDK_DEBUG environment variable: if set to "1", "true", or "yes", debug-level messages are emitted. Otherwise only info and above are shown.
This is a convenience for troubleshooting; production code should inject a structured logger (SlogLogger or a custom implementation).
type LoggerFunc ¶
type LoggerFunc struct {
DebugFunc func(msg string, kv ...any)
InfoFunc func(msg string, kv ...any)
WarnFunc func(msg string, kv ...any)
ErrorFunc func(msg string, kv ...any)
}
LoggerFunc adapts a plain function into a Logger. Each level can be nil to disable that level.
type PrivateToken ¶
type PrivateToken struct{ Token string }
PrivateToken sets the GitLab-style "PRIVATE-TOKEN" header.
func (PrivateToken) Apply ¶
func (p PrivateToken) Apply(req *http.Request)
Apply implements AuthStrategy.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter provides proactive rate limiting by tracking X-RateLimit-* headers returned by the server. It combines a configurable requests-per-second cap (enforced by golang.org/x/time/rate) with adaptive throttling when the server reports low remaining quota.
RateLimiter is safe for concurrent use.
func NewRateLimiter ¶
func NewRateLimiter(opts ...RateLimiterOption) *RateLimiter
NewRateLimiter creates a RateLimiter with the given options.
func (*RateLimiter) Remaining ¶
func (rl *RateLimiter) Remaining() int
Remaining returns the last seen X-RateLimit-Remaining value.
func (*RateLimiter) ResetAt ¶
func (rl *RateLimiter) ResetAt() time.Time
ResetAt returns the last seen X-RateLimit-Reset time.
func (*RateLimiter) UpdateFromResponse ¶
func (rl *RateLimiter) UpdateFromResponse(resp *http.Response)
UpdateFromResponse reads X-RateLimit-* headers from the response to update the internal state. Call this after every HTTP response.
func (*RateLimiter) Wait ¶
func (rl *RateLimiter) Wait()
Wait blocks until the next request is allowed. It considers both the configured RPS cap and the adaptive throttling based on remaining quota. Use WaitContext if you need cancellation support.
func (*RateLimiter) WaitContext ¶
func (rl *RateLimiter) WaitContext(ctx context.Context) error
WaitContext blocks until the next request is allowed or the context is cancelled. It returns ctx.Err() if the context is done before the wait completes. It considers both the configured RPS cap and the adaptive throttling based on remaining quota.
The lock is only held to compute the wait duration and to reserve a local quota slot; the actual sleep happens outside the critical section, so a long adaptive wait does not block other waiters, UpdateFromResponse calls, or state inspection.
type RateLimiterOption ¶
type RateLimiterOption func(*RateLimiter)
RateLimiterOption configures a RateLimiter.
func WithMinDelay ¶
func WithMinDelay(d time.Duration) RateLimiterOption
WithMinDelay sets the minimum delay between consecutive requests.
func WithRPS ¶
func WithRPS(rps float64) RateLimiterOption
WithRPS sets the maximum requests per second. A value <= 0 means unlimited.
func WithThrottleThreshold ¶
func WithThrottleThreshold(n int) RateLimiterOption
WithThrottleThreshold sets the X-RateLimit-Remaining threshold below which the limiter starts adding delays to avoid hitting the limit. Default is 10.
type Request ¶
type Request struct {
// Method is the HTTP method (GET, POST, PUT, PATCH, DELETE, ...).
Method string
// Path is the request path relative to Client.BaseURL. It may include a
// query string; in that case Query is ignored.
Path string
// Query holds additional query parameters. Entries with an empty value are
// dropped.
Query url.Values
// Headers carries per-request headers. The auth strategy may overwrite
// any of these.
Headers http.Header
// Body is the request payload. Supported types:
// - nil: no body
// - []byte / *bytes.Buffer / *bytes.Reader / *strings.Reader: sent verbatim
// - io.Reader: streamed as-is
// - any other: JSON-encoded
Body any
// Result is the target for JSON decoding of the response body. When nil
// (and StatusCode is not 204), the raw body is still available via
// Response.Body.
Result any
}
Request describes a single HTTP call. It is intentionally a plain value type so callers can build and pass it around without lock-in to a fluent API.
type RequestHook ¶
RequestHook is invoked just before a request is sent. It can mutate the context (e.g. add tracing metadata) or the request (e.g. add headers). Hooks are run in registration order. The first non-nil error returned by a hook aborts the request.
Hooks observe the in-flight request, but they do NOT have visibility into retries. Each retry uses a fresh request (and therefore re-invokes the hooks) only when the round tripper is wrapped with retry behavior. The default Client.Do uses retry internally; if you need end-to-end visibility including retries, use Client.RoundTripper() and configure retries via Client.NewRetryingRoundTripper().
type Response ¶
Response is the result of a single HTTP call. The body has been read in full and is available to the caller. It is the caller's responsibility to cap the body size upstream (e.g. via a wrapping RoundTripper) when feeding untrusted responses.
type ResponseHook ¶
type ResponseHook func(ctx context.Context, req *http.Request, resp *http.Response, duration time.Duration, err error)
ResponseHook is invoked after a response is received or an error occurred. It cannot change the outcome. Use it for logging, metrics, or tracing.
type RetryConfig ¶
type RetryConfig struct {
// MaxAttempts is the total number of attempts (including the first).
// <= 0 disables retry.
MaxAttempts int
// BaseDelay is the initial backoff delay. Defaults to 500ms when <= 0.
BaseDelay time.Duration
// MaxDelay caps the backoff delay. Defaults to 30s when <= 0. It also
// caps any Retry-After header sent by the server.
MaxDelay time.Duration
// Statuses lists extra status codes that should trigger a retry, in
// addition to the default 429 and 5xx.
Statuses []int
// RetryWrite re-enables status-based retries for non-idempotent methods
// (POST, PATCH, ...). It is false by default because replaying a write
// after "executed but failed" (e.g. 502 on resource creation) can
// duplicate server-side effects. Set it only when the target APIs are
// known to deduplicate writes or tolerate duplicates.
RetryWrite bool
}
RetryConfig controls exponential-backoff retry on transient HTTP failures.
A request is retried when the response status is in {429, 5xx} or matches one of the Statuses entries. The delay between attempts is
delay = min(BaseDelay * 2^(attempt-1), MaxDelay) * jitter
where jitter is uniform in [0.75, 1.25]. A Retry-After header on the response, if present and parseable, takes precedence over the calculated delay; it is clamped to MaxDelay so a hostile header cannot suspend the caller for an unbounded time.
Retries are method-aware. Only idempotent methods {GET, HEAD, PUT, DELETE, OPTIONS} are retried on retryable statuses or ambiguous network errors. A non-idempotent request (e.g. POST) that already reached the server must not be replayed: a 502 after a successful create would duplicate the resource. Non-idempotent requests are therefore only retried when the error proves the request never went out on the wire (DNS resolution or dial failure). Set RetryWrite to opt in to status-based retries for writes.
func DefaultRetryConfig ¶
func DefaultRetryConfig() RetryConfig
DefaultRetryConfig returns a sane default: 3 attempts, 500ms base, 30s cap.
func (*RetryConfig) Backoff ¶
Backoff returns the delay to wait before the given attempt (1-indexed, so Backoff(1) is the delay before the first retry). The Retry-After header on resp, if set and parseable, takes precedence — clamped to the resolved MaxDelay so an unbounded server-provided delay cannot hang the caller.
func (*RetryConfig) Do ¶
func (rc *RetryConfig) Do(ctx context.Context, client *http.Client, req *http.Request, logger Logger, maxBodySize int64) (*http.Response, []byte, error)
Do executes a single request through the underlying client, applying the configured retry policy. The response body is fully read so it can be replayed across attempts. The returned http.Response has a fresh body reader attached so the caller can read it once and then receive io.EOF.
Retries respect the request method: see RetryConfig for the exact rules.
func (*RetryConfig) ShouldRetry ¶
func (rc *RetryConfig) ShouldRetry(status int) bool
ShouldRetry reports whether the given status code should trigger a retry. This is the status-only predicate; call sites that know the request must additionally gate on the method via canRetryStatus.
type SlogLogger ¶
SlogLogger adapts a *slog.Logger to the Logger interface.
type StaticAuth ¶
StaticAuth lets callers inject an arbitrary header value.
func (StaticAuth) Apply ¶
func (s StaticAuth) Apply(req *http.Request)
Apply implements AuthStrategy.
type TokenHeader ¶
type TokenHeader struct{ Token string }
TokenHeader sets the older "Authorization: token <token>" header used by some GitHub-compatible APIs.
func (TokenHeader) Apply ¶
func (t TokenHeader) Apply(req *http.Request)
Apply implements AuthStrategy.