gocurl

package module
v0.0.0-...-9ad8d68 Latest Latest
Warning

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

Go to latest
Published: Jun 21, 2026 License: MIT Imports: 34 Imported by: 0

README

GoCurl

Paste any curl command from any API doc straight into Go. Test it in the shell, run the exact same command in your code — no translation, no guesswork.

resp, err := gocurl.Curl(ctx, `
  curl https://api.github.com/repos/golang/go
`)

GoCurl is a production-grade, curl-ergonomic HTTP client for Go, built on net/http, with a CLI that shares the exact same syntax. It removes the tax every Go developer pays when integrating a new API — mentally compiling a curl snippet from the docs into http.NewRequest, headers, body encoding, and auth — and backs it with the retries, circuit breaking, observability, SSRF protection, secret redaction, and typed errors that real integrations need in production.

Persuasion by example, not by marketing. Every performance and reliability claim in this repo cites a test or benchmark you can run yourself — enforced by an automated doc-lint (TestDocHonestyLint), not left to good intentions. That is the motto, and it runs through the README, VISION, docs, and book alike.

See VISION.md for what we're building and why.

Project status

Production-grade, pre-1.0. The engine is hardened and tested — race-clean, fuzzed, with a coverage gate in CI, streaming bodies, connection pooling, and the resilience/observability/ security stack below. The remaining pre-1.0 caveat is the contract, not the quality: the public API may still change and curl-flag coverage is still expanding, so pin a version and check the CHANGELOG when upgrading. Feedback and contributions are very welcome.

Proven, not promised

"Production-grade" and "mission-critical" are claims we back with tests you can run, not adjectives. A two-tier fault-injection harness deliberately breaks the network and asserts gocurl does the right thing:

  • Bounded retriesWithTimeout bounds the whole operation including backoff, not just one attempt (TestFault_OverallRetryBudget).
  • Idempotency-aware retries, including HTTP/2 GOAWAY/RST_STREAM (TestFault_H2ErrorsRetried).
  • Graceful shutdown never truncates a live stream and is never wedged by a panicking middleware (TestFault_ShutdownWaitsForOpenBody, TestFaultT2_PanicMiddlewareDoesNotWedgeShutdown).
  • Memory bounds against a decompression bomb from an untrusted server (TestFault_BufferingHelpersBoundedAgainstBomb).
  • Secrets never leak on any failure path (TestFault_NoSecretLeakOnFailurePaths).
  • No leaks under sustained load (TestClient_Soak); backpressure without deadlock (TestClient_Soak_Backpressure).
  • Wire-parity with real curl, proven by differential testing (TestCurlParity_DifferentialVsRealCurl).

Performance is reported honestly: gocurl targets parity with a well-tuned net/http client (proven by BenchmarkRoundTrip_Gocurl_Prepared and TestLatencyDistribution) and we publish where it loses. See docs/operations.md, docs/benchmarking.md, and the v1.0-readiness checklist. Every claim above is checked by an automated honesty doc-lint (TestDocHonestyLint): no claim ships without a named, un-skipped test.

Why gocurl over hand-rolled net/http

A net/http purist can write everything gocurl does — but almost nobody writes it correctly, per service, and keeps it correct. Because gocurl receives the curl recipe, it knows your intent and wires the right execution pipeline around it:

Concern Hand-rolled net/http gocurl
Overall timeout across retries Client.Timeout is per-attempt; easy to ship a retry amplifier WithTimeout bounds the whole loop, backoff clamped to remaining budget
Retry safety Retry-everything (replays a non-idempotent POST) or retry-nothing Idempotency-aware; only safe methods + retryable outcomes
Error decisions err != nil, then guess Classified Kind (errors.As into h2/DNS/TLS errors)
Secret hygiene One careless fmt.Errorf("%v", url) leaks a token Redaction on every error/log/span path, build-gated
Untrusted-server memory DisableCompression quietly removes net/http's guard Bounded buffered reads + 1 MiB header cap

You paste a curl command from the API docs and get this pipeline for free — the operations guide shows how to tune it.

Why GoCurl

Every REST API documents itself with curl. Almost none ship a Go SDK for their long-tail endpoints. GoCurl makes the curl command be the code, so you can:

  • Integrate a third-party API by copy-pasting its documented curl example — then run it in production with retries, timeouts, tracing, and metrics around it.
  • Operate service-to-service HTTP through a pooled Client with circuit breaking, rate limiting, and SSRF protection.
  • Write scripts, CI checks, and API smoke tests with one syntax for shell and code.
  • Drive HTTP from config by storing curl commands as data and executing them.

Built on net/http and organized around parse once, execute manyPrepare a request once, then Do it repeatedly over a pooled Client — so the per-request overhead above hand-written net/http is small and constant. The goal is parity with a well-tuned net/http client plus the ergonomics and reliability above; we make no "faster than net/http" claims.

Installation

As a library:

go get github.com/maniartech/gocurl

As a command-line tool:

go install github.com/maniartech/gocurl/cmd/gocurl@latest

Requires Go 1.23+.

Usage

As a library

The primary entry points accept a curl command (as a single string or as separate arguments) and return a standard *http.Response:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/maniartech/gocurl"
)

func main() {
    ctx := context.Background()

    // Separate arguments (each is one token, like os.Args).
    resp, err := gocurl.Curl(ctx,
        "-H", "Accept: application/vnd.github+json",
        "https://api.github.com/repos/golang/go",
    )
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    fmt.Println(resp.StatusCode)
}

Convenience helpers read the body for you:

// Body as a string (plus the response).
body, resp, err := gocurl.CurlString(ctx, "https://api.github.com/repos/golang/go")

// Decode JSON directly into a struct.
var repo struct {
    FullName string `json:"full_name"`
    Stars    int    `json:"stargazers_count"`
}
_, err = gocurl.CurlJSON(ctx, &repo, "https://api.github.com/repos/golang/go")

// Stream a download to a file.
n, resp, err := gocurl.CurlDownload(ctx, "go.tar.gz",
    "https://go.dev/dl/go1.23.0.linux-amd64.tar.gz")

CurlBytes is also available for raw []byte bodies.

Reusable client

For repeated calls, create a Client once and reuse it — it pools connections and carries shared configuration (timeouts, retries, auth, TLS, observability). Functional options configure it:

client, err := gocurl.New(
    gocurl.WithTimeout(10*time.Second),
    gocurl.WithRetryAttempts(3),
    gocurl.WithUserAgent("myapp/1.0"),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Run a curl command directly...
resp, err := client.Curl(ctx, "curl https://api.github.com/repos/golang/go")

// ...or prepare once and execute it many times (safe across goroutines).
req, err := client.Prepare("curl -H 'Accept: application/json' https://api.example.com/v1/items")
if err != nil {
    log.Fatal(err)
}
resp, err = client.Do(ctx, req)
Typed request building

Prefer typed, IDE-discoverable configuration over a curl string? Assemble a request with the options builder and run it with Execute:

opts := options.NewRequestOptionsBuilder().
    SetURL("https://api.example.com/v1/items").
    SetMethod("POST").
    AddHeader("Authorization", "Bearer "+token).
    SetBody(`{"name":"widget"}`).
    Build()

resp, err := gocurl.Execute(ctx, opts)
Variable substitution

By default, environment variables ($VAR and ${VAR}) are expanded automatically:

resp, err := gocurl.Curl(ctx,
    "-H", "Authorization: Bearer $GITHUB_TOKEN",
    "https://api.github.com/user",
)

For explicit, testable control — and to avoid pulling in the process environment — pass a Variables map and use the WithVars entry points:

vars := gocurl.Variables{"token": myToken}
resp, err := gocurl.CurlWithVars(ctx, vars,
    "-H", "Authorization: Bearer ${token}",
    "https://api.github.com/user",
)
Command-line interface

The CLI uses the same curl syntax as the library:

gocurl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user
gocurl -X POST -d "name=value" https://httpbin.org/post
gocurl -o repo.json https://api.github.com/repos/golang/go

Run gocurl with no arguments for usage help.

Built for real integrations

GoCurl is more than a parser — the reusable Client wires in the cross-cutting concerns real API integrations need. Everything below is opt-in via functional options on gocurl.New(...):

  • Resilience — idempotency-aware retries with backoff (WithRetry, WithRetryAttempts, WithRetryBudget), a circuit breaker (WithCircuitBreaker), and a client-side rate limiter (WithRateLimit).
  • Observability — pluggable tracing, metrics, structured logging, and lifecycle hooks (WithTracer, WithMetrics, WithLogger, WithHooks), with ready-made OpenTelemetry and Prometheus adapters in observability/otel and observability/prometheus.
  • Security — an opt-in SSRF guard (WithSSRFGuard), automatic redaction of secrets in errors and verbose output, a fail-closed policy for plaintext auth, and TLS certificate pinning.
  • Typed errors — every failure is a classifiable *GocurlError. Branch with errors.Is(err, gocurl.ErrTimeout) (also ErrConnect, ErrTLS, ErrSSRFBlocked, ErrCircuitOpen, …) or gocurl.KindOf(err); gocurl.IsRetryable(err) reports whether a retry could help.
  • Streaming & limits — responses stream by default (the library never buffers the full body or writes to stdout), with an optional response body-size cap to bound memory.

Supported curl features

GoCurl targets the HTTP/HTTPS flags that appear in real API documentation, including: HTTP methods (-X), headers (-H), data/body (-d), form and file upload (-F), basic and bearer auth (-u), output to file (-o), TLS options (--cert, --key, --cacert, -k), proxies (-x, including SOCKS5), compression (--compressed), and HTTP version selection (--http2, --http2-only, --http1.1; --http1.0/-0 is accepted as a best-effort 1.1 pin + Connection: close, since Go's net/http cannot emit a true HTTP/1.0 request line).

It deliberately does not implement curl's non-HTTP protocols (FTP, SMTP, etc.) or flags that don't map to HTTP API usage. Flag coverage is expanding — see VISION.md.

Contributing

Contributions are welcome. The most valuable work right now is parser correctness (making real-world curl commands from API docs run verbatim) and test coverage. Please open an issue to discuss substantial changes before sending a PR. See CONTRIBUTING.md to get started.

License

MIT — see LICENSE.

Documentation

Overview

Package gocurl is a curl-ergonomic HTTP client for Go, built on net/http.

It lets you paste a curl command from any API's documentation straight into Go code (or run the identical command with the gocurl CLI), removing the translation tax of rewriting curl snippets as net/http requests.

Quick start

The primary entry point accepts a curl command as a single string or as separate arguments and returns a standard *http.Response:

resp, err := gocurl.Curl(ctx,
	"-H", "Accept: application/vnd.github+json",
	"https://api.github.com/repos/golang/go")
if err != nil {
	return err
}
defer resp.Body.Close()

Convenience helpers read the body for you:

body, resp, err := gocurl.CurlString(ctx, "https://api.github.com/repos/golang/go")
_, err = gocurl.CurlJSON(ctx, &v, "https://api.github.com/repos/golang/go")
n, resp, err := gocurl.CurlDownload(ctx, "go.tar.gz", "https://go.dev/dl/...")

Variables

By default, environment variables ($VAR and ${VAR}) are expanded. For explicit, testable control that does not read the process environment, pass a Variables map to the WithVars entry points:

vars := gocurl.Variables{"token": myToken}
resp, err := gocurl.CurlWithVars(ctx, vars,
	"-H", "Authorization: Bearer ${token}",
	"https://api.github.com/user")

Scope

gocurl targets curl's HTTP/HTTPS behavior for the flags that appear in real API documentation. It is a convenience layer on top of net/http, not a replacement for it, and it does not implement curl's non-HTTP protocols.

Index

Constants

View Source
const (
	CompressionGzip    = "gzip"
	CompressionDeflate = "deflate"
	CompressionBrotli  = "br"
)

Compression types supported

View Source
const (
	// MaxPooledResponseSize is the maximum size for pooled buffers (1MB)
	MaxPooledResponseSize = 1024 * 1024
	// StreamingThreshold is when we switch to streaming mode (1MB)
	StreamingThreshold = 1024 * 1024
)
View Source
const DefaultMaxReplayBytes int64 = 1 << 20 // 1 MiB

DefaultMaxReplayBytes is the default ceiling for buffering a request body for retries when the body cannot be re-obtained via GetBody. Bodies larger than this are sent once and the request becomes non-retryable on attempt 2+.

Variables

View Source
var (
	ErrParse          error = &kindSentinel{KindParse}
	ErrValidation     error = &kindSentinel{KindValidation}
	ErrConnect        error = &kindSentinel{KindConnect}
	ErrTLS            error = &kindSentinel{KindTLS}
	ErrTimeout        error = &kindSentinel{KindTimeout}
	ErrCanceled       error = &kindSentinel{KindCanceled}
	ErrServerStatus   error = &kindSentinel{KindServerStatus}
	ErrRetryExhausted error = &kindSentinel{KindRetryExhausted}
	ErrBodyRead       error = &kindSentinel{KindBodyRead}
)

Sentinel errors for use with errors.Is. They never wrap anything; they exist purely so callers can write errors.Is(err, gocurl.ErrTimeout) without importing or type-asserting the concrete type.

View Source
var ErrCircuitOpen = errors.New("gocurl: circuit breaker is open")

ErrCircuitOpen is returned by the circuit breaker middleware when the circuit for a request's key is open (fast-fail). It is classified non-retryable.

View Source
var ErrSSRFBlocked = errors.New("blocked by SSRF policy")

ErrSSRFBlocked is wrapped by every SSRF policy rejection so callers can match it with errors.Is. The wrapping GocurlError is classified KindValidation (non-retryable). See specs/07-security.md.

View Source
var ErrTooManyRedirects = errors.New("too many redirects")

ErrTooManyRedirects is wrapped by the redirect-cap error so callers (and the CLI's exit-code mapping) can match it with errors.Is even after net/http wraps it in a *url.Error and the engine wraps that in a GocurlError.

View Source
var Version = "dev"

Version is the current version of the gocurl library. This can be set at build time using:

go build -ldflags "-X github.com/maniartech/gocurl.Version=1.0.0"

If not set, defaults to "dev".

Functions

func BodyReadError

func BodyReadError(url string, err error) error

BodyReadError reports a failure reading/decoding the response body, including the over-limit (truncation) case (Kind == KindBodyRead).

func BytesBody

func BytesBody(b []byte) options.BodySource

BytesBody returns a rewindable BodySource backed by an in-memory byte slice.

func CanceledError

func CanceledError(url string, err error) error

CanceledError reports a context cancellation by the caller (Kind == KindCanceled).

func ConnectError

func ConnectError(url string, err error) error

ConnectError reports a dial/DNS/proxy-connect failure (Kind == KindConnect).

func Curl

func Curl(ctx context.Context, command ...string) (*http.Response, error)

Curl executes an HTTP request using curl-compatible syntax.

Automatically detects input format: - Single argument: Parsed as shell command (supports multi-line, backslashes, comments) - Multiple arguments: Treated as separate arguments (no parsing)

Environment variables ($VAR and ${VAR}) are automatically expanded from os.Environ. For explicit control, use CurlCommand() or CurlArgs().

Examples:

// Variadic (auto-detected)
resp, err := Curl(ctx, "-H", "X-Token: abc", "https://example.com")
defer resp.Body.Close()

// Shell command (auto-detected)
resp, err := Curl(ctx, `curl -H 'X-Token: abc' https://example.com`)
defer resp.Body.Close()

// Multi-line (auto-detected)
resp, err := Curl(ctx, `
  curl -X POST https://api.example.com \
    -H 'Content-Type: application/json' \
    -d '{"key":"value"}'
`)
defer resp.Body.Close()

// Access response details
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header.Get("Content-Type"))

func CurlArgs

func CurlArgs(ctx context.Context, args ...string) (*http.Response, error)

CurlArgs executes a curl command from variadic arguments. Each argument is a separate token (like os.Args). Environment variables are automatically expanded.

func CurlArgsWithVars

func CurlArgsWithVars(ctx context.Context, vars Variables, args ...string) (*http.Response, error)

CurlArgsWithVars executes variadic arguments with explicit variables

func CurlBytes

func CurlBytes(ctx context.Context, command ...string) ([]byte, *http.Response, error)

CurlBytes executes request and returns body as []byte + response

func CurlBytesArgs

func CurlBytesArgs(ctx context.Context, args ...string) ([]byte, *http.Response, error)

CurlBytesArgs executes variadic args and returns body as []byte + response

func CurlBytesCommand

func CurlBytesCommand(ctx context.Context, command string) ([]byte, *http.Response, error)

CurlBytesCommand executes shell command and returns body as []byte + response

func CurlCommand

func CurlCommand(ctx context.Context, command string) (*http.Response, error)

CurlCommand executes a curl command from a shell-style string. Handles multi-line commands, backslash continuations, and comments. Environment variables are automatically expanded.

func CurlCommandWithVars

func CurlCommandWithVars(ctx context.Context, vars Variables, command string) (*http.Response, error)

CurlCommandWithVars executes a shell-style command with explicit variables

func CurlDownload

func CurlDownload(ctx context.Context, filepath string, command ...string) (int64, *http.Response, error)

CurlDownload executes request and saves body to file, returns bytes written + response

func CurlDownloadArgs

func CurlDownloadArgs(ctx context.Context, filepath string, args ...string) (int64, *http.Response, error)

CurlDownloadArgs executes variadic args and saves body to file

func CurlDownloadCommand

func CurlDownloadCommand(ctx context.Context, filepath string, command string) (int64, *http.Response, error)

CurlDownloadCommand executes shell command and saves body to file

func CurlJSON

func CurlJSON(ctx context.Context, v interface{}, command ...string) (*http.Response, error)

CurlJSON executes request and decodes JSON response into provided struct

func CurlJSONArgs

func CurlJSONArgs(ctx context.Context, v interface{}, args ...string) (*http.Response, error)

CurlJSONArgs executes variadic args and decodes JSON response

func CurlJSONCommand

func CurlJSONCommand(ctx context.Context, v interface{}, command string) (*http.Response, error)

CurlJSONCommand executes shell command and decodes JSON response

func CurlString

func CurlString(ctx context.Context, command ...string) (string, *http.Response, error)

CurlString executes request and returns body as string + response

func CurlStringArgs

func CurlStringArgs(ctx context.Context, args ...string) (string, *http.Response, error)

CurlStringArgs executes variadic args and returns body as string + response

func CurlStringCommand

func CurlStringCommand(ctx context.Context, command string) (string, *http.Response, error)

CurlStringCommand executes shell command and returns body as string + response

func CurlWithVars

func CurlWithVars(ctx context.Context, vars Variables, command ...string) (*http.Response, error)

CurlWithVars executes a curl command with explicit variable map. Variables are NOT expanded from environment, only from the provided map. Useful for testing and security-critical code.

func Execute

func Execute(ctx context.Context, opts *options.RequestOptions) (*http.Response, error)

Execute runs a request described by a typed *options.RequestOptions and returns the live response. It is the programmatic counterpart to the curl-string Curl* helpers: use it when you build the request with the options builder (options.NewRequestOptionsBuilder().…​.Build()) or by populating options.RequestOptions directly, rather than from a pasted curl command.

The response body is streamed and is subject to opts.ResponseBodyLimit; the caller owns reading and closing resp.Body. With opts.FailOnError set, a >=400 status is returned as a typed error alongside the still-inspectable response (see failOnStatus). For a reusable, middleware-configured client, prefer Client.Prepare/Do; Execute is the one-shot path and shares its engine.

func ExpandVariables

func ExpandVariables(text string, vars Variables) (string, error)

ExpandVariables replaces ${var} or $var with values from map Supports escaping: \${var} becomes literal ${var}

func FileBody

func FileBody(path string) options.BodySource

FileBody returns a rewindable BodySource that streams a file from disk (re-opened for each retry instead of being buffered in memory).

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether gocurl's resilience layer considers the outermost GocurlError safe to retry for an idempotent request.

func IsSensitiveHeader

func IsSensitiveHeader(headerName string) bool

IsSensitiveHeader reports whether a header value should be redacted in logs, verbose output, spans, and errors. It first consults the canonical exact-match set, then falls back to a bounded heuristic: no fixed list can enumerate the open set of vendor auth headers (X-Goog-Api-Key, Private-Token, X-Vault-Token, X-Functions-Key, …), so anything that looks credential-bearing — by content or by a credential-style suffix — is redacted. This errs toward redaction, which is the intended fail-safe (Spec 07: never leak secrets).

func IsTemporary

func IsTemporary(err error) bool

IsTemporary reports whether the outermost GocurlError considers the failure plausibly transient. Advisory only — not a retry decision (see IsRetryable).

func IsTimeout

func IsTimeout(err error) bool

IsTimeout reports whether err (anywhere in its chain) was caused by a deadline/timeout. It matches a KindTimeout GocurlError as well as a bare context.DeadlineExceeded, so it resolves through a retry-exhausted wrapper.

func LoadCookiesFromFile

func LoadCookiesFromFile(filePath string) ([]*http.Cookie, error)

LoadCookiesFromFile is a utility function to load cookies from a file into a slice of http.Cookie objects.

func MultipartBody

func MultipartBody(fields map[string]string, files ...MultipartFile) options.BodySource

MultipartBody returns a streaming multipart/form-data BodySource. The body is produced lazily through an io.Pipe; closing the returned reader unblocks the writer goroutine, so an aborted/cancelled request never leaks it.

func ParseError

func ParseError(command string, err error) error

ParseError creates a parsing error (Kind == KindParse).

func ReaderBody

func ReaderBody(r io.Reader) options.BodySource

ReaderBody returns a NON-rewindable BodySource that streams from r once. It cannot be replayed for retries; use BytesBody/FileBody when retries are needed.

func RedactCommand

func RedactCommand(cmd string) string

RedactCommand returns a copy of a curl command safe to log or display, with credentials (`-u`, Authorization/Bearer/Basic, Cookie, sensitive URL params) redacted. It is the exported entry point to sanitizeCommand.

func RedactHeaders

func RedactHeaders(headers map[string][]string) map[string][]string

RedactHeaders creates a copy of headers with sensitive values redacted

func RedactURL

func RedactURL(raw string) string

RedactURL returns a copy of raw safe to log or display: sensitive query parameters and any basic-auth userinfo are redacted. It is the exported entry point to the single URL-redaction path (sanitizeURL).

func RequestError

func RequestError(url string, err error) error

RequestError creates a request execution error. The wrapped transport error is classified (KindConnect/KindTLS/KindTimeout/KindCanceled) rather than blindly tagged, while the Op stays "request" for message compatibility.

func ResponseError

func ResponseError(url string, err error) error

ResponseError creates a response reading error (Kind == KindBodyRead).

func RetryError

func RetryError(url string, attempts int, err error) error

RetryError creates a retry exhaustion error (Kind == KindRetryExhausted). It chains the last attempt's (already classified) error so errors.Is(err, ErrTimeout) and friends still resolve through the wrapper.

func RoundTripperFromHandler

func RoundTripperFromHandler(h Handler) http.RoundTripper

RoundTripperFromHandler adapts a Handler into an http.RoundTripper.

func SaveCookiesToFile

func SaveCookiesToFile(filePath string, cookies []*http.Cookie) error

SaveCookiesToFile writes cookies to a file in Netscape format

func ServerStatusError

func ServerStatusError(url string, status int) error

ServerStatusError reports a non-2xx HTTP response surfaced as an error. It is only produced when fail-on-status is enabled (WithFailOnStatus / -f); the engine still returns the *http.Response alongside it so the caller may read the error body.

func StringBody

func StringBody(s string) options.BodySource

StringBody returns a rewindable BodySource backed by a string.

func TLSError

func TLSError(url string, err error) error

TLSError reports a TLS handshake/verification/pin failure (Kind == KindTLS).

func TimeoutError

func TimeoutError(url string, err error) error

TimeoutError reports a deadline exceeded (Kind == KindTimeout).

func ValidationError

func ValidationError(field string, err error) error

ValidationError creates an input validation error (Kind == KindValidation).

Types

type Attempt

type Attempt struct {
	Method   string         // request method
	Number   int            // 1-based attempt index
	Response *http.Response // nil on transport error
	Err      error          // nil on HTTP response
	Started  time.Time      // when this attempt began
}

Attempt is the decision input passed to a custom Retryable function.

type Backoff

type Backoff interface {
	Delay(attempt int, rnd *rand.Rand) time.Duration
}

Backoff computes the delay before the retry that follows a given attempt (1-based). Implementations are pure except for jitter, which draws from the supplied *rand.Rand so tests can inject a seeded source for determinism.

func ConstantBackoff

func ConstantBackoff(d time.Duration) Backoff

ConstantBackoff returns a Backoff that always waits d (mirrors the legacy RetryDelay-when-set behavior).

func ExponentialJitter

func ExponentialJitter(base, max time.Duration) Backoff

ExponentialJitter returns an exponential backoff: base*2^(attempt-1) capped at max, with equal jitter (each delay is in [d/2, d]). Equal jitter (rather than AWS full jitter) preserves a usable lower bound on the delay.

type BreakerConfig

type BreakerConfig struct {
	FailureThreshold float64                          // fraction of failures in the window that trips (default 0.5)
	MinRequests      int                              // minimum samples before tripping (default 20)
	Window           time.Duration                    // rolling window (default 10s)
	OpenTimeout      time.Duration                    // delay before a half-open probe (default 5s)
	KeyFunc          func(*http.Request) string       // circuit key (default: request URL host)
	IsFailure        func(*http.Response, error) bool // failure predicate (default: err != nil || status >= 500)
}

BreakerConfig configures a CircuitBreaker. Zero-valued fields take sensible defaults (see CircuitBreaker).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a reusable, concurrency-safe HTTP client. Configure it once with functional Options, then execute prepared Requests many times — connections are pooled and reused across calls. A Client owns its transport, so closing one Client never affects another.

See specs/01-client-api.md.

func New

func New(opts ...Option) (*Client, error)

New constructs a Client from functional Options.

func (*Client) Close

func (c *Client) Close() error

Close releases idle connections held by the Client's transport. It does NOT abort in-flight requests; use Shutdown to drain them first.

func (*Client) Curl

func (c *Client) Curl(ctx context.Context, command string) (*http.Response, error)

Curl prepares (parsing env vars) and executes a curl command in one call.

func (*Client) CurlBytes

func (c *Client) CurlBytes(ctx context.Context, command string) ([]byte, *http.Response, error)

CurlBytes executes a curl command and returns the body as bytes.

func (*Client) CurlDownload

func (c *Client) CurlDownload(ctx context.Context, filePath, command string) (int64, *http.Response, error)

CurlDownload executes a curl command and streams the body to filePath.

func (*Client) CurlJSON

func (c *Client) CurlJSON(ctx context.Context, v interface{}, command string) (*http.Response, error)

CurlJSON executes a curl command and decodes the JSON response into v.

func (*Client) CurlString

func (c *Client) CurlString(ctx context.Context, command string) (string, *http.Response, error)

CurlString executes a curl command and returns the body as a string.

func (*Client) Do

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

Do executes a prepared Request and returns the live response. The caller owns resp.Body and must Close it. The body is streamed (never buffered or written to stdout by the Client).

func (*Client) Prepare

func (c *Client) Prepare(command string) (*Request, error)

Prepare parses a curl command ONCE into a reusable Request. Environment variables ($VAR/${VAR}) are expanded, mirroring CurlCommand.

func (*Client) PrepareNoEnv

func (c *Client) PrepareNoEnv(command string) (*Request, error)

PrepareNoEnv parses a curl command without expanding environment variables.

func (*Client) PrepareWithVars

func (c *Client) PrepareWithVars(vars Variables, command string) (*Request, error)

PrepareWithVars parses a curl command, expanding $VAR/${VAR} ONLY from the supplied map (never the process environment).

func (*Client) Shutdown

func (c *Client) Shutdown(ctx context.Context) error

Shutdown marks the Client closed, waits for in-flight requests to finish (or the context to expire), then releases idle connections.

type Field

type Field struct {
	Key   string
	Value any
}

Field is a single structured key/value pair for logs and span attributes. Values placed in a Field that leaves the process via a sink must already be redacted; the core only ever passes pre-redacted values.

func Duration

func Duration(k string, v time.Duration) Field

Duration builds a time.Duration Field.

func Err

func Err(err error) Field

Err builds an error Field under the key "error" (nil-safe).

func Int

func Int(k string, v int) Field

Int builds an int Field.

func String

func String(k, v string) Field

String builds a string Field.

type GocurlError

type GocurlError struct {
	Op      string // Operation: "parse", "request", "response", "retry", etc. (legacy/message)
	Kind    Kind   // machine-readable classification
	Command string // Command snippet (sanitized)
	URL     string // Request URL (sanitized)
	Status  int    // HTTP status when Kind == KindServerStatus, else 0
	Attempt int    // attempts made when Kind == KindRetryExhausted, else 0
	Err     error  // Underlying error
}

GocurlError provides structured error information with context. It is the single concrete error type gocurl returns; callers branch on Kind (the machine-readable discriminator) or use errors.Is with the Err* sentinels, and errors.As to recover this type for full detail.

See specs/08-error-model.md.

func (*GocurlError) Error

func (e *GocurlError) Error() string

Error implements the error interface. The message shape is backward compatible (op: url=… cmd=… underlying); the status is appended to the op segment for KindServerStatus. The joined message is run through scrubErrorString so credentials leaking in from wrapped stdlib errors never appear.

func (*GocurlError) Is

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

Is reports whether target is the kindSentinel matching this error's Kind. errors.Is walks the Unwrap chain for us, so a retry-exhausted error wrapping a timeout still matches errors.Is(err, ErrTimeout), and errors.Is(err, context.DeadlineExceeded) keeps resolving through the wrapped error.

func (*GocurlError) Retryable

func (e *GocurlError) Retryable() bool

Retryable reports whether gocurl's resilience layer considers this error safe to retry for an idempotent request: connect and timeout always, server-status only for the retryable status codes. TLS and retry-exhausted are terminal.

func (*GocurlError) Temporary

func (e *GocurlError) Temporary() bool

Temporary reports whether the failure is plausibly transient. Advisory and conservative; it is NOT a retry decision on its own (idempotency still governs retries — see Spec 04).

func (*GocurlError) Timeout

func (e *GocurlError) Timeout() bool

Timeout reports whether the error was caused by a deadline/timeout. It is nil-Err safe and consults a wrapped net.Error / context.DeadlineExceeded.

func (*GocurlError) Unwrap

func (e *GocurlError) Unwrap() error

Unwrap allows errors.Is and errors.As to work

type Handler

type Handler func(*http.Request) (*http.Response, error)

Handler executes a single HTTP request and returns its response. It has the same shape as http.RoundTripper.RoundTrip but as a function, so behaviors can be composed. The innermost Handler in a Client's chain is backed by the Client's pooled net/http transport (the request execution engine).

See specs/12-middleware.md.

func HandlerFromRoundTripper

func HandlerFromRoundTripper(rt http.RoundTripper) Handler

HandlerFromRoundTripper adapts an http.RoundTripper into a Handler.

type Hooks

type Hooks struct {
	OnRequest  func(ctx context.Context, req *http.Request)
	OnRetry    func(ctx context.Context, req *http.Request, attempt int, lastErr error, lastResp *http.Response)
	OnResponse func(ctx context.Context, req *http.Request, resp *http.Response, elapsed time.Duration)
	OnError    func(ctx context.Context, req *http.Request, err error, kind Kind)
}

Hooks are optional, synchronous lifecycle callbacks. Each is nil-safe. They receive the live request/response (which the caller already owns); anything a hook forwards to a sink must be redacted by the caller.

type Kind

type Kind int

Kind classifies a GocurlError. It is the machine-readable discriminator that callers branch on instead of string-matching error messages; the legacy Op string is retained for backward-compatible message output.

See specs/08-error-model.md.

const (
	// KindUnknown is the zero value: an error that was not (or could not be)
	// classified. A struct-literal GocurlError built without a constructor
	// reports KindUnknown.
	KindUnknown Kind = iota
	// KindParse — tokenize/convert of a curl command failed.
	KindParse
	// KindValidation — validateRequestOptions rejected the prepared request.
	KindValidation
	// KindConnect — dial / connection refused / DNS / proxy connect failure.
	KindConnect
	// KindTLS — handshake, certificate verification, or pin mismatch.
	KindTLS
	// KindTimeout — a deadline was exceeded (connect, per-attempt, or overall).
	KindTimeout
	// KindCanceled — the context was canceled by the caller.
	KindCanceled
	// KindServerStatus — a 4xx/5xx response surfaced as an error (opt-in only,
	// via WithFailOnStatus / -f).
	KindServerStatus
	// KindRetryExhausted — all retry attempts failed.
	KindRetryExhausted
	// KindBodyRead — reading/decoding the response body failed (incl. over-limit).
	KindBodyRead
)

func KindOf

func KindOf(err error) Kind

KindOf returns the Kind of the first (outermost) GocurlError in err's chain, or KindUnknown if there is none.

func (Kind) String

func (k Kind) String() string

String returns the lowercase, human-readable name of the kind. It also serves as the default Op segment for errors built from the typed constructors.

type Level

type Level int8

Level is a structured log severity.

const (
	LevelDebug Level = iota
	LevelInfo
	LevelWarn
	LevelError
)

func (Level) String

func (l Level) String() string

String returns the lowercase level name.

type Limiter

type Limiter interface {
	Wait(ctx context.Context) error
}

Limiter throttles outbound requests. Wait blocks until the request may proceed or the context is done (returning ctx.Err()). It is the pluggable seam for rate limiting: the built-in TokenBucket implements it, and an external limiter (e.g. golang.org/x/time/rate) can be supplied via RateLimiter.

type Logger

type Logger interface {
	Log(ctx context.Context, level Level, msg string, fields ...Field)
}

Logger is a minimal structured logger. Implementations must be safe for concurrent use. A nil Logger disables logging.

type Metrics

type Metrics interface {
	IncRequest(info RequestInfo)                     // total logical requests started
	IncInFlight(delta int)                           // +1 at start, -1 at end
	ObserveLatency(d time.Duration, info ResultInfo) // wall time to response headers
	IncRetry(info RequestInfo)                       // one per retry attempt beyond the first
	IncError(kind Kind, info RequestInfo)            // one per failed logical request
}

Metrics collects request-level measurements. Implementations must be safe for concurrent use and must not block. A nil Metrics disables metrics. Labels are low-cardinality and pre-redacted (method, host, status, error kind only).

type Middleware

type Middleware func(next Handler) Handler

Middleware wraps a Handler to add cross-cutting behavior (retry, tracing, redaction, SSRF guard, …). Middlewares compose so that the FIRST middleware passed is the OUTERMOST: it runs first on the way out and last on the way back.

func CircuitBreaker

func CircuitBreaker(cfg BreakerConfig) Middleware

CircuitBreaker returns a Middleware implementing a per-key (default: per-host) rolling-window circuit breaker. When the failure fraction in the window exceeds the threshold (after MinRequests samples), the circuit opens and requests fast-fail with ErrCircuitOpen; after OpenTimeout a single probe is allowed (half-open), and its outcome closes or re-opens the circuit. The breaker counts only the FINAL outcome of a request (e.g. the result after the retry loop), never individual retry attempts.

func FromMiddlewareFunc

func FromMiddlewareFunc(f middlewares.MiddlewareFunc) Middleware

FromMiddlewareFunc adapts a legacy request-mutating middlewares.MiddlewareFunc (func(*http.Request) (*http.Request, error)) into a Middleware, preserving the behavior of options.RequestOptions.Middleware.

func RateLimiter

func RateLimiter(l Limiter) Middleware

RateLimiter returns a Middleware that blocks each request on l.Wait(ctx) before passing it to the next handler. A nil Limiter is a pass-through.

func SSRFGuard

func SSRFGuard(policy SSRFPolicy) Middleware

SSRFGuard returns a Middleware that runs the SSRF policy pre-flight check on the initial request before it leaves the chain.

type MultipartFile

type MultipartFile struct {
	Field    string
	FileName string
	Path     string
	Reader   io.Reader
}

MultipartFile describes one file part of a multipart/form-data body. Prefer Path (re-openable, so the body is rewindable for retries); Reader is a fallback that makes the body non-rewindable.

type Option

type Option func(*config) error

Option configures a Client. Options are applied in order by New().

func WithAllowInsecureAuth

func WithAllowInsecureAuth(allow bool) Option

WithAllowInsecureAuth opts out of the fail-closed plaintext-auth check, so BasicAuth / a bearer token may be sent over cleartext http:// (equivalent to GOCURL_ALLOW_INSECURE_AUTH=1). Off by default.

func WithCircuitBreaker

func WithCircuitBreaker(cfg BreakerConfig) Option

WithCircuitBreaker installs a per-key (default per-host) circuit breaker that fast-fails with ErrCircuitOpen while the circuit is open. It wraps the whole retry loop, counting only the final outcome of each request.

func WithConnectTimeout

func WithConnectTimeout(d time.Duration) Option

WithConnectTimeout sets the connection (dial) timeout.

func WithCookieFile

func WithCookieFile(path string) Option

WithCookieFile sets a cookie jar file used by the Client (curl -b/-c).

func WithDefaultHeader

func WithDefaultHeader(key, value string) Option

WithDefaultHeader adds a default header applied to every request that does not already set that header.

func WithExpectContinueTimeout

func WithExpectContinueTimeout(d time.Duration) Option

WithExpectContinueTimeout bounds the wait for a 100-continue response.

func WithFailOnStatus

func WithFailOnStatus(fail bool) Option

WithFailOnStatus enables curl -f/--fail behavior: a response with status >= 400 is returned as a ServerStatusError. The live *http.Response is still returned alongside the error so the caller can read the error body. Off by default — a non-2xx status is normally not an error.

func WithFollowRedirects

func WithFollowRedirects(follow bool) Option

WithFollowRedirects enables/disables following redirects for requests that do not specify their own policy (e.g. a curl command without -L).

func WithHTTP2

func WithHTTP2(enabled bool) Option

WithHTTP2 enables or disables HTTP/2 upgrade over TLS (default enabled).

func WithHTTP2Only

func WithHTTP2Only(allowCleartext bool) Option

WithHTTP2Only forces HTTP/2 exclusively. If allowCleartext is true, HTTP/2 cleartext (h2c) is permitted for non-TLS targets.

HTTP/3 (QUIC) is intentionally out of scope; it is a documented future add-on.

func WithHTTP11

func WithHTTP11() Option

WithHTTP11 pins HTTP/1.1, suppressing HTTP/2 negotiation. Mutually exclusive with the HTTP/2 options (last one set wins).

There is deliberately no WithHTTP10: curl's --http1.0 implies Connection: close, which would defeat a reusable Client's connection pool on every request. The --http1.0 curl flag is supported on the one-shot Curl* path instead.

func WithHooks

func WithHooks(h Hooks) Option

WithHooks sets the lifecycle hooks.

func WithIdleConnTimeout

func WithIdleConnTimeout(d time.Duration) Option

WithIdleConnTimeout sets how long an idle connection is kept before closing.

func WithInsecure

func WithInsecure(insecure bool) Option

WithInsecure disables TLS certificate verification (curl -k). Use only for testing; it is unsafe in production.

func WithLogger

func WithLogger(l Logger) Option

WithLogger sets the structured Logger (nil disables logging).

func WithMaxConnsPerHost

func WithMaxConnsPerHost(n int) Option

WithMaxConnsPerHost limits the total connections per host (dialing blocks when reached). 0 means unlimited (net/http semantics).

func WithMaxIdleConns

func WithMaxIdleConns(n int) Option

WithMaxIdleConns sets the maximum number of idle (keep-alive) connections across all hosts. 0 means no limit.

func WithMaxIdleConnsPerHost

func WithMaxIdleConnsPerHost(n int) Option

WithMaxIdleConnsPerHost sets the maximum idle connections kept per host.

func WithMaxRedirects

func WithMaxRedirects(n int) Option

WithMaxRedirects sets the maximum number of redirects to follow.

func WithMaxReplayBytes

func WithMaxReplayBytes(n int64) Option

WithMaxReplayBytes caps how many bytes of a request body are buffered for retries when the body cannot be re-obtained via GetBody. Bodies larger than n are sent once and become non-retryable. n == 0 means unlimited; the default is DefaultMaxReplayBytes (1 MiB).

func WithMetrics

func WithMetrics(m Metrics) Option

WithMetrics sets the request Metrics collector (nil disables metrics).

func WithMiddleware

func WithMiddleware(mw ...Middleware) Option

WithMiddleware appends user middleware to the Client's chain. The first middleware passed is the outermost.

func WithProxy

func WithProxy(proxyURL string) Option

WithProxy sets a proxy URL (http://, https://, or socks5://) for all requests.

func WithRateLimit

func WithRateLimit(rps float64, burst int) Option

WithRateLimit installs a client-side token-bucket rate limiter admitting rps requests per second with the given burst. It is the outermost-but-one middleware (inside the circuit breaker), so requests block on a token unless the circuit is already open.

func WithRedirectPolicy

func WithRedirectPolicy(p RedirectPolicy) Option

WithRedirectPolicy sets the redirect policy. If Follow is true and Max is 0, Max defaults to 30 (curl-like).

func WithRequestIDFunc

func WithRequestIDFunc(fn func() string) Option

WithRequestIDFunc sets a generator used to produce an X-Request-ID when a request does not already carry one.

func WithResponseHeaderTimeout

func WithResponseHeaderTimeout(d time.Duration) Option

WithResponseHeaderTimeout bounds the wait for response headers after the request is written. 0 means no timeout.

func WithRetry

func WithRetry(p RetryPolicy) Option

WithRetry sets the Client's default RetryPolicy. Retries are idempotency-aware: by default only GET/HEAD/OPTIONS/TRACE/PUT/DELETE (or a request carrying an Idempotency-Key header) are retried — a POST is NOT retried unless AllowMethods opts it in. This differs from the legacy options.RetryConfig / --retry path, which remains method-agnostic.

func WithRetryAttempts

func WithRetryAttempts(n int) Option

WithRetryAttempts is sugar for WithRetry(DefaultRetryPolicy(n)).

func WithRetryBudget

func WithRetryBudget(ratio, minPerSec float64) Option

WithRetryBudget attaches a retry budget (token bucket) limiting retries to the given fraction of request volume, with a per-second floor. It applies to the effective policy when that policy does not already carry its own Budget.

func WithSSRFGuard

func WithSSRFGuard(policy SSRFPolicy) Option

WithSSRFGuard enables the opt-in SSRF guard with the given policy. It blocks the initial request and every redirect hop whose host resolves to a policy-blocked address (loopback/link-local/private/cloud-metadata) unless allow-listed. Use DefaultSSRFPolicy() for the recommended setting.

func WithTLSConfig

func WithTLSConfig(cfg *tls.Config) Option

WithTLSConfig supplies a *tls.Config that loadTLSConfig merges over the secure defaults for every request (curl flags still apply on top). Use for custom root pools, client certs, or pinning beyond the curl-flag surface.

func WithTLSHandshakeTimeout

func WithTLSHandshakeTimeout(d time.Duration) Option

WithTLSHandshakeTimeout bounds the TLS handshake duration.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the overall per-request timeout ceiling (http.Client.Timeout). A per-request --max-time or context deadline may impose a shorter bound.

func WithTracer

func WithTracer(t Tracer) Option

WithTracer sets the request Tracer (nil disables tracing).

func WithTransport

func WithTransport(rt http.RoundTripper) Option

WithTransport overrides the Client's owned transport with a custom http.RoundTripper (advanced; useful for tests, mocks, or a hand-tuned transport). When set, TLS/proxy options that build a transport are ignored.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent sets a default User-Agent for requests that do not set their own.

type PersistentCookieJar

type PersistentCookieJar struct {
	// contains filtered or unexported fields
}

PersistentCookieJar implements http.CookieJar with file persistence Thread-safe implementation for concurrent access

func NewPersistentCookieJar

func NewPersistentCookieJar(filePath string) (*PersistentCookieJar, error)

NewPersistentCookieJar creates a new cookie jar with file persistence. If filePath is not empty, it will load cookies from that file.

func (*PersistentCookieJar) Cookies

func (pcj *PersistentCookieJar) Cookies(u *url.URL) []*http.Cookie

Cookies implements http.CookieJar interface

func (*PersistentCookieJar) Load

func (pcj *PersistentCookieJar) Load() error

Load reads cookies from a Netscape-format cookie file. Compatible with curl's cookie file format.

func (*PersistentCookieJar) Save

func (pcj *PersistentCookieJar) Save() error

Save persists cookies to the file in Netscape cookie file format. This format is compatible with curl's --cookie and --cookie-jar options.

func (*PersistentCookieJar) SetCookies

func (pcj *PersistentCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie)

SetCookies implements http.CookieJar interface

type RedirectPolicy

type RedirectPolicy struct {
	Follow bool
	Max    int
	Allow  func(req *http.Request, via []*http.Request) error
}

RedirectPolicy controls how the Client follows HTTP redirects. Allow, if set, is an additional per-redirect authorization hook (e.g. an SSRF guard, Spec 07) that runs after the follow/max checks.

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request is an immutable, prepared request template — the "parse once" artifact of the parse-once/execute-many model. Build it from a curl command with Client.Prepare* or programmatically with NewRequest, then execute it many times with Client.Do. Builder methods (WithHeader, WithQuery, …) return a modified COPY, so a Request is safe to share across goroutines.

See specs/02-request-model-and-curl-compat.md.

func NewRequest

func NewRequest(method, rawURL string, opts ...RequestOption) (*Request, error)

NewRequest builds a Request programmatically (no curl parsing). The URL is normalized (a missing scheme defaults to http://, matching the curl path).

func (*Request) AddHeader

func (r *Request) AddHeader(key, value string) *Request

AddHeader returns a copy of the request with the header value appended.

func (*Request) Clone

func (r *Request) Clone() *Request

Clone returns an independent copy of the request.

func (*Request) Method

func (r *Request) Method() string

Method returns the HTTP method of the prepared request.

func (*Request) Options

func (r *Request) Options() *options.RequestOptions

Options returns a deep copy of the underlying request options. The returned value may be modified freely without affecting the Request.

func (*Request) URL

func (r *Request) URL() string

URL returns the URL of the prepared request.

func (*Request) WithBody

func (r *Request) WithBody(b []byte) *Request

WithBody returns a copy of the request with the given raw body.

func (*Request) WithBodyFile

func (r *Request) WithBodyFile(path string) *Request

WithBodyFile returns a copy of the request that streams its body from a file.

func (*Request) WithBodySource

func (r *Request) WithBodySource(src options.BodySource) *Request

WithBodySource returns a copy of the request with a streaming body source, overriding any raw body.

func (*Request) WithHeader

func (r *Request) WithHeader(key, value string) *Request

WithHeader returns a copy of the request with the header set (replacing any existing values for that key).

func (*Request) WithQuery

func (r *Request) WithQuery(key, value string) *Request

WithQuery returns a copy of the request with the query parameter set.

func (*Request) WithRetryPolicy

func (r *Request) WithRetryPolicy(p RetryPolicy) *Request

WithRetryPolicy returns a copy of the request with a per-request RetryPolicy that overrides the Client's default for this request only.

func (*Request) WithVars

func (r *Request) WithVars(vars Variables) *Request

WithVars returns a copy of the request re-parsed with the given variables. It only applies to requests built from a curl command (Prepare*); for a programmatic NewRequest it returns an unchanged copy.

type RequestInfo

type RequestInfo struct {
	Method string
	Host   string // host only — never the full URL (cardinality + secrets)
}

RequestInfo carries low-cardinality, pre-redacted request labels.

type RequestOption

type RequestOption func(*options.RequestOptions) error

RequestOption configures a Request built by NewRequest.

func Body

func Body(b []byte) RequestOption

Body sets a raw request body on a NewRequest.

func BodyReader

func BodyReader(r io.Reader) RequestOption

BodyReader sets a request body from a reader. It is buffered; for true streaming (and retry replay) use Stream with a BodySource.

func Header(key, value string) RequestOption

Header sets a header on a NewRequest.

func Query

func Query(key, value string) RequestOption

Query sets a query parameter on a NewRequest.

func Stream

func Stream(src options.BodySource) RequestOption

Stream sets a streaming BodySource on a NewRequest (e.g. FileBody, ReaderBody, MultipartBody), overriding any raw body.

type ResponseReader

type ResponseReader struct {
	// contains filtered or unexported fields
}

ResponseReader wraps a response for efficient reading

func NewResponseReader

func NewResponseReader(resp *http.Response) *ResponseReader

NewResponseReader creates a reader for efficient response handling

func (*ResponseReader) Bytes

func (rr *ResponseReader) Bytes() ([]byte, error)

Bytes returns the full body as bytes

func (*ResponseReader) Read

func (rr *ResponseReader) Read(p []byte) (int, error)

Read implements io.Reader for streaming

func (*ResponseReader) Reset

func (rr *ResponseReader) Reset()

Reset allows the reader to be reused

func (*ResponseReader) String

func (rr *ResponseReader) String() (string, error)

String returns the body as a string

type ResultInfo

type ResultInfo struct {
	RequestInfo
	StatusCode int  // 0 if no response was received
	Kind       Kind // KindUnknown on success
}

ResultInfo carries the outcome labels for a completed logical request. Kind is KindUnknown on success.

type RetryBudget

type RetryBudget struct {
	// Ratio is the maximum retries expressed as a fraction of requests (e.g. 0.1
	// allows retries up to 10% of request volume). Each successful request adds
	// Ratio tokens.
	Ratio float64
	// MinPerSec is a floor on token accrual (tokens/sec) so a low-traffic client
	// can still retry occasionally even with little baseline traffic.
	MinPerSec float64
	// contains filtered or unexported fields
}

RetryBudget caps retries as a fraction of overall request volume so that a partial outage cannot trigger a retry storm. It is a token bucket: each completed request refills it a little, and each retry spends a token. When the bucket is empty, further retries are suppressed (the first attempt of any request is never budget-gated).

A RetryBudget is safe for concurrent use and is shared across all requests of a Client. See specs/04-resilience.md.

func NewRetryBudget

func NewRetryBudget(ratio, minPerSec float64) *RetryBudget

NewRetryBudget returns a retry budget that allows retries up to `ratio` of request volume, with a floor of `minPerSec` tokens/sec for low-traffic clients. The bucket starts full.

func (*RetryBudget) Consume

func (b *RetryBudget) Consume() bool

Consume attempts to spend one retry token. It returns true (and decrements) if a token was available, false otherwise. A nil budget always permits the retry.

func (*RetryBudget) Refund

func (b *RetryBudget) Refund()

Refund credits the budget for a completed request (Ratio tokens), raising the allowance for subsequent retries. A nil budget is a no-op.

type RetryPolicy

type RetryPolicy struct {
	MaxAttempts       int                 // total attempts incl. the first; <=1 disables retries
	Backoff           Backoff             // delay schedule (default: ExponentialJitter(100ms,5s))
	MaxElapsed        time.Duration       // 0 = unbounded; overall wall-clock budget across attempts
	PerAttempt        time.Duration       // 0 = none; per-attempt deadline via context.WithTimeout
	RetryOnStatus     []int               // HTTP status codes to retry (nil => 429,500,502,503,504)
	RespectRetryAfter bool                // honor Retry-After on 429/503
	AllowMethods      []string            // methods eligible for retry; nil => idempotent set
	Retryable         func(*Attempt) bool // optional override; when set it FULLY governs the retry decision, bypassing the idempotency gate (a non-replayable body still cannot be retried)
	Budget            *RetryBudget        // optional token-bucket cap on retry fraction (nil = unlimited)
	// contains filtered or unexported fields
}

RetryPolicy is an immutable value describing how a request is retried. Attach it to a Client with WithRetry / WithRetryAttempts, or to a single prepared Request with Request.WithRetryPolicy. The zero value disables retries (MaxAttempts <= 1 means a single attempt).

Retries are idempotency-aware by default: only methods in the idempotent set (GET, HEAD, OPTIONS, TRACE, PUT, DELETE) — or a request carrying a non-empty Idempotency-Key header — are retried. POST/PATCH/CONNECT are NOT retried unless AllowMethods opts them in. (The legacy options.RetryConfig / --retry path remains method-agnostic for backward compatibility.)

See specs/04-resilience.md.

func DefaultRetryPolicy

func DefaultRetryPolicy(maxAttempts int) RetryPolicy

DefaultRetryPolicy returns the recommended idempotency-aware policy for the given attempt count: exponential jitter (100ms base, 5s cap), retrying 429/500/502/503/504, honoring Retry-After.

type SSRFPolicy

type SSRFPolicy struct {
	BlockLoopback      bool     // 127.0.0.0/8, ::1
	BlockLinkLocal     bool     // 169.254.0.0/16, fe80::/10
	BlockPrivate       bool     // RFC1918, fc00::/7 (ULA)
	BlockCloudMetadata bool     // 169.254.169.254, fd00:ec2::254, metadata.google.internal
	AllowHosts         []string // explicit allow-list (host or host:port), checked first
	AllowIPs           []string // explicit allow-list of IPs or CIDRs, checked first
}

SSRFPolicy controls which destinations a Client may reach. It is OPT-IN (WithSSRFGuard); the default Client enforces nothing, preserving the paste-any-curl promise. Enforcement happens at two points: a pre-flight middleware on the initial request, and a per-redirect check on every hop (so a public URL that 302s to an internal address is still blocked).

func DefaultSSRFPolicy

func DefaultSSRFPolicy() SSRFPolicy

DefaultSSRFPolicy blocks loopback, link-local, private, and cloud-metadata destinations — the recommended setting for untrusted curl input.

func (SSRFPolicy) CheckSSRF

func (p SSRFPolicy) CheckSSRF(ctx context.Context, host string) error

CheckSSRF resolves host and rejects the request if any resolved IP is blocked by the policy and not on the allow-list. A resolution failure is NOT a policy block (it surfaces later as a normal dial error). host may be "host" or "host:port" (bracketed IPv6 accepted).

type Span

type Span interface {
	SetAttributes(attrs ...Field)
	AddEvent(name string, attrs ...Field)
	RecordError(err error)
	End()
}

Span is the active span for an in-flight request. End is always called once.

type TokenBucket

type TokenBucket struct {
	// contains filtered or unexported fields
}

TokenBucket is a client-side token-bucket rate limiter. It refills at `rps` tokens per second up to a maximum of `burst`, and is safe for concurrent use. It has zero external dependencies.

func NewTokenBucket

func NewTokenBucket(rps float64, burst int) *TokenBucket

NewTokenBucket returns a token bucket admitting `rps` requests per second with a burst capacity of `burst`. The bucket starts full. rps <= 0 is treated as 1; burst < 1 is treated as 1.

func (*TokenBucket) Wait

func (tb *TokenBucket) Wait(ctx context.Context) error

Wait blocks until a token is available or ctx is done. On cancellation it returns ctx.Err() without consuming a token.

type Tracer

type Tracer interface {
	// StartSpan returns a child context carrying the span and the span itself.
	StartSpan(ctx context.Context, name string, attrs ...Field) (context.Context, Span)
}

Tracer creates a span around one logical request (covering all retry attempts). A nil Tracer disables tracing.

type Variables

type Variables map[string]string

Variables type for safe variable substitution

Directories

Path Synopsis
cmd
gocurl command
internal
corpus
Package corpus holds the curl-compat regression corpus: real command strings copied verbatim from public API docs, each mapped to the parse result it must produce.
Package corpus holds the curl-compat regression corpus: real command strings copied verbatim from public API docs, each mapped to the parse result it must produce.
Package tests holds black-box tests that exercise the public gocurl API and the gocurl CLI exactly as an external consumer would — across HTTP methods, flags, and options.
Package tests holds black-box tests that exercise the public gocurl API and the gocurl CLI exactly as an external consumer would — across HTTP methods, flags, and options.
Package tokenizer splits a curl command string into shell-style tokens, respecting single/double quotes and backslash escapes.
Package tokenizer splits a curl command string into shell-style tokens, respecting single/double quotes and backslash escapes.

Jump to

Keyboard shortcuts

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