Help us shape the future of Go! Take the 2026 Go Developer Survey

transport

package
v0.63.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 18 Imported by: 0

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:

  1. 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.

  2. 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

View Source
const DefaultMaxBodySize = 10 * 1024 * 1024

DefaultMaxBodySize is the default maximum response body size (10 MB).

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout is the per-request HTTP timeout applied when the caller does not specify one explicitly.

Variables

View Source
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

func NewStatusError

func NewStatusError(method, path string, status int, body []byte) error

NewStatusError builds an error for an HTTP response with status >= 400. The returned error is an *Error with the request method, path, status code and response body captured for diagnostics.

Types

type AuthFunc

type AuthFunc func(req *http.Request)

AuthFunc adapts a plain function into an AuthStrategy.

func (AuthFunc) Apply

func (f AuthFunc) Apply(req *http.Request)

Apply implements AuthStrategy.

type AuthStrategy

type AuthStrategy interface {
	Apply(req *http.Request)
}

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

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

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

func (c *Client) DoJSON(ctx context.Context, req *Request) (*Response, error)

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

func (c *Client) DoRaw(ctx context.Context, req *Request) ([]byte, error)

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) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Is

func (e *Error) Is(target error) bool

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) IsClientError

func (e *Error) IsClientError() bool

IsClientError reports 4xx.

func (*Error) IsServerError

func (e *Error) IsServerError() bool

IsServerError reports 5xx.

func (*Error) IsStatus

func (e *Error) IsStatus(code int) bool

IsStatus reports whether the error has the given status code.

func (*Error) IsStatusClass

func (e *Error) IsStatusClass(lo int) bool

IsStatusClass reports whether the error is in the given status class. For example IsStatusClass(http.StatusInternalServerError) reports 5xx.

func (*Error) StatusCode

func (e *Error) StatusCode() int

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.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap implements errors.Unwrap so callers can recover the underlying cause.

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

func (h *Hooks) ExecuteRequest(ctx context.Context, req *http.Request) error

ExecuteRequest runs every registered request hook in order. The first non-nil error short-circuits. A nil *Hooks is a no-op.

func (*Hooks) ExecuteResponse

func (h *Hooks) ExecuteResponse(ctx context.Context, req *http.Request, resp *http.Response, duration time.Duration, err error)

ExecuteResponse runs every registered response hook in order. Hooks themselves do not affect the outcome. 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).

func NoopLogger

func NoopLogger() Logger

NoopLogger returns a logger that discards every event.

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.

func (LoggerFunc) Debug

func (l LoggerFunc) Debug(msg string, kv ...any)

Debug implements Logger.

func (LoggerFunc) Error

func (l LoggerFunc) Error(msg string, kv ...any)

Error implements Logger.

func (LoggerFunc) Info

func (l LoggerFunc) Info(msg string, kv ...any)

Info implements Logger.

func (LoggerFunc) Warn

func (l LoggerFunc) Warn(msg string, kv ...any)

Warn implements Logger.

type None

type None struct{}

None is the no-op auth strategy.

func (None) Apply

func (None) Apply(*http.Request)

Apply implements AuthStrategy.

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

type RequestHook func(ctx context.Context, req *http.Request) error

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

type Response struct {
	StatusCode int
	Header     http.Header
	Body       []byte
}

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

func (rc *RetryConfig) Backoff(attempt int, resp *http.Response) time.Duration

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

type SlogLogger struct{ Logger *slog.Logger }

SlogLogger adapts a *slog.Logger to the Logger interface.

func (SlogLogger) Debug

func (l SlogLogger) Debug(msg string, kv ...any)

Debug implements Logger.

func (SlogLogger) Error

func (l SlogLogger) Error(msg string, kv ...any)

Error implements Logger.

func (SlogLogger) Info

func (l SlogLogger) Info(msg string, kv ...any)

Info implements Logger.

func (SlogLogger) Warn

func (l SlogLogger) Warn(msg string, kv ...any)

Warn implements Logger.

type StaticAuth

type StaticAuth struct {
	Header string
	Value  string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL