Documentation
¶
Overview ¶
Package httpc provides a production-grade HTTP client for JSON API communication.
Features:
- All standard HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
- Form-encoded POST (Client.PostForm, Client.PostFormWithHeader) with JSON-decoded responses, for OAuth2 token endpoints and similar APIs
- Base URL and client-wide default headers (WithBaseURL, WithDefaultHeaders)
- JSON encode/decode via github.com/gtkit/json/v2 (build-tag swappable backend)
- Connection pooling, keep-alive, HTTP/2
- Bounded body draining for connection reuse
- Response body size cap to guard against memory exhaustion
- No built-in logging: every outcome (status, headers, error) is returned to the caller, who logs with full business context
- Context propagation and cancellation
A nil request body — including a typed nil pointer, map, or slice stored in the any parameter — sends no body and no Content-Type, rather than the JSON literal "null".
Retries are intentionally left to the caller: only the caller knows which requests are idempotent and what backoff policy fits. Request bodies are replayable (GetBody is set), so a caller-side retry can resend safely.
Reuse: a Client is concurrency-safe and read-only after construction. Build one with New and keep it for the process lifetime (or per module) — do not call New per request. The connection pool (keep-alive, HTTP/2) lives on the underlying transport; rebuilding the client each request defeats reuse and piles up TLS handshakes and TIME_WAIT. For a single high-concurrency upstream, raise WithMaxIdleConnsPerHost; cap a fragile upstream with WithMaxConnsPerHost.
Quick start:
c := httpc.New(httpc.WithTimeout(10 * time.Second)) var result MyResponse status, err := c.PostJSON(ctx, url, reqBody, &result)
Index ¶
- Constants
- Variables
- type Client
- func (c *Client) DeleteJSON(ctx context.Context, url string, body any, result any) (int, error)
- func (c *Client) DeleteJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
- func (c *Client) Do(req *http.Request) (*http.Response, error)
- func (c *Client) GetJSON(ctx context.Context, url string, headers map[string]string, result any) (int, error)
- func (c *Client) GetJSONWithHeader(ctx context.Context, url string, headers map[string]string, result any) (http.Header, int, error)
- func (c *Client) GetRaw(ctx context.Context, url string, headers map[string]string) ([]byte, int, error)
- func (c *Client) GetRawWithHeader(ctx context.Context, url string, headers map[string]string) (http.Header, []byte, int, error)
- func (c *Client) HTTPClient() *http.Client
- func (c *Client) Head(ctx context.Context, url string, headers map[string]string) (*http.Response, error)
- func (c *Client) PatchJSON(ctx context.Context, url string, body any, result any) (int, error)
- func (c *Client) PatchJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
- func (c *Client) PostForm(ctx context.Context, url string, headers map[string]string, form url.Values, ...) (int, error)
- func (c *Client) PostFormWithHeader(ctx context.Context, url string, headers map[string]string, form url.Values, ...) (http.Header, int, error)
- func (c *Client) PostJSON(ctx context.Context, url string, body any, result any) (int, error)
- func (c *Client) PostJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
- func (c *Client) PostRaw(ctx context.Context, url string, body any) ([]byte, int, error)
- func (c *Client) PostRawWithHeader(ctx context.Context, url string, body any) (http.Header, []byte, int, error)
- func (c *Client) PutJSON(ctx context.Context, url string, body any, result any) (int, error)
- func (c *Client) PutJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
- func (c *Client) RequestJSON(ctx context.Context, method, url string, headers map[string]string, body any, ...) (int, error)
- func (c *Client) RequestJSONWithHeader(ctx context.Context, method, url string, headers map[string]string, body any, ...) (http.Header, int, error)
- func (c *Client) RequestRaw(ctx context.Context, method, url string, headers map[string]string, body any) ([]byte, int, error)
- func (c *Client) RequestRawWithHeader(ctx context.Context, method, url string, headers map[string]string, body any) (http.Header, []byte, int, error)
- type Option
- func WithBaseURL(u string) Option
- func WithCheckRedirect(fn func(req *http.Request, via []*http.Request) error) Option
- func WithDefaultHeaders(h map[string]string) Option
- func WithHTTPClient(hc *http.Client) Option
- func WithMaxConnsPerHost(n int) Option
- func WithMaxIdleConnsPerHost(n int) Option
- func WithMaxResponseBytes(n int64) Option
- func WithTimeout(d time.Duration) Option
- func WithTransport(t http.RoundTripper) Option
- func WithoutRedirect() Option
Examples ¶
Constants ¶
const Version = "v1.3.2"
Variables ¶
var ErrEmptyBody = errors.New("httpc: empty response body")
ErrEmptyBody is returned by the JSON convenience methods when the server sends an empty (or whitespace-only) response body but a decode target was provided. It lets callers distinguish "no content" (e.g. 204, an empty 502) from malformed JSON. Detect it with errors.Is. Use the Raw methods if an empty body is expected and should not be treated as an error.
var ErrResponseTooLarge = errors.New("httpc: response body exceeds limit")
ErrResponseTooLarge is returned (wrapped) when a response body exceeds the configured maximum. See WithMaxResponseBytes. Detect it with errors.Is.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a concurrency-safe HTTP client optimized for JSON APIs. All fields are read-only after construction — safe to share across goroutines.
By design httpc does not log: every outcome (status, headers, error) is returned to the caller, who should log it from their own layer with full business context. For transport-level tracing (DNS, TLS, connection reuse), attach a net/http/httptrace.ClientTrace to the request context.
func New ¶
New creates a new Client with production-grade defaults.
Example ¶
package main
import (
"fmt"
"time"
"github.com/gtkit/httpc"
)
func main() {
client := httpc.New(
httpc.WithTimeout(5*time.Second),
httpc.WithBaseURL("https://api.example.com"),
)
fmt.Println(client.HTTPClient().Timeout)
}
Output: 5s
func (*Client) DeleteJSON ¶
DeleteJSON sends DELETE with optional JSON body and JSON-decodes the response.
func (*Client) DeleteJSONWithHeader ¶
func (c *Client) DeleteJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
DeleteJSONWithHeader sends DELETE with an optional JSON body, JSON-decodes the response, and returns the response headers.
func (*Client) Do ¶
Do executes an arbitrary http.Request and returns the response. The caller is responsible for closing the response body. Prefer the higher-level methods unless you need full control.
func (*Client) GetJSON ¶
func (c *Client) GetJSON(ctx context.Context, url string, headers map[string]string, result any) (int, error)
GetJSON sends GET with custom headers and JSON-decodes the response.
func (*Client) GetJSONWithHeader ¶
func (c *Client) GetJSONWithHeader(ctx context.Context, url string, headers map[string]string, result any) (http.Header, int, error)
GetJSONWithHeader sends GET with custom headers, JSON-decodes the response, and returns the response headers.
func (*Client) GetRaw ¶
func (c *Client) GetRaw(ctx context.Context, url string, headers map[string]string) ([]byte, int, error)
GetRaw sends GET and returns the raw response body bytes. Useful when the response needs multiple unmarshal passes.
func (*Client) GetRawWithHeader ¶
func (c *Client) GetRawWithHeader(ctx context.Context, url string, headers map[string]string) (http.Header, []byte, int, error)
GetRawWithHeader sends GET and returns the raw response body together with the response headers. See the *WithHeader JSON methods for header semantics.
func (*Client) HTTPClient ¶
HTTPClient returns the underlying http.Client for advanced usage.
func (*Client) Head ¶
func (c *Client) Head(ctx context.Context, url string, headers map[string]string) (*http.Response, error)
Head sends a HEAD request and returns the response headers.
The returned response's Body has already been drained and closed — read resp.Header and resp.StatusCode only; reading resp.Body returns an error.
func (*Client) PatchJSONWithHeader ¶
func (c *Client) PatchJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
PatchJSONWithHeader sends PATCH with a JSON body, JSON-decodes the response, and returns the response headers.
func (*Client) PostForm ¶
func (c *Client) PostForm(ctx context.Context, url string, headers map[string]string, form url.Values, result any) (int, error)
PostForm sends a POST request with the form encoded as application/x-www-form-urlencoded and JSON-decodes the response into result. Typical use is OAuth2 token endpoints and other form-based APIs that reply with JSON.
Content-Type and Accept are set automatically; headers follows the same precedence as the JSON methods (standard headers, then client-wide defaults, then per-call headers — later wins), so a per-call Authorization (e.g. Basic auth) overrides a default one. A nil or empty form sends no body and no Content-Type, mirroring the JSON methods' nil-body semantics. The body is replayable (GetBody is set), so caller-side retries can resend safely.
Response semantics match the JSON methods: an empty body with a non-nil result returns ErrEmptyBody; a nil result skips reading the body entirely (fire-and-forget); bodies over the configured cap return an error wrapping ErrResponseTooLarge.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"github.com/gtkit/httpc"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"grant_type":%q}`, r.PostFormValue("grant_type"))
}))
defer server.Close()
client := httpc.New(httpc.WithHTTPClient(server.Client()))
form := url.Values{"grant_type": {"client_credentials"}}
var result struct {
GrantType string `json:"grant_type"`
}
status, err := client.PostForm(context.Background(), server.URL, nil, form, &result)
fmt.Println(status, result.GrantType, err == nil)
}
Output: 200 client_credentials true
func (*Client) PostFormWithHeader ¶
func (c *Client) PostFormWithHeader(ctx context.Context, url string, headers map[string]string, form url.Values, result any) (http.Header, int, error)
PostFormWithHeader behaves like Client.PostForm and additionally returns the response headers. The header is non-nil whenever a response was received (including on decode failure); it is nil only on transport-level errors.
func (*Client) PostJSON ¶
PostJSON sends POST with a JSON body and JSON-decodes the response.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/gtkit/httpc"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()
client := httpc.New(httpc.WithHTTPClient(server.Client()))
var result struct {
OK bool `json:"ok"`
}
status, err := client.PostJSON(context.Background(), server.URL, map[string]string{"name": "gtkit"}, &result)
fmt.Println(status, result.OK, err == nil)
}
Output: 200 true true
func (*Client) PostJSONWithHeader ¶
func (c *Client) PostJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
PostJSONWithHeader sends POST with a JSON body, JSON-decodes the response, and returns the response headers.
func (*Client) PostRawWithHeader ¶
func (c *Client) PostRawWithHeader(ctx context.Context, url string, body any) (http.Header, []byte, int, error)
PostRawWithHeader sends POST with a JSON body and returns the raw response body together with the response headers.
func (*Client) PutJSONWithHeader ¶
func (c *Client) PutJSONWithHeader(ctx context.Context, url string, body any, result any) (http.Header, int, error)
PutJSONWithHeader sends PUT with a JSON body, JSON-decodes the response, and returns the response headers.
func (*Client) RequestJSON ¶
func (c *Client) RequestJSON(ctx context.Context, method, url string, headers map[string]string, body any, result any) (int, error)
RequestJSON sends any HTTP method with custom headers, optional JSON body, and JSON-decodes the response.
func (*Client) RequestJSONWithHeader ¶
func (c *Client) RequestJSONWithHeader(ctx context.Context, method, url string, headers map[string]string, body any, result any) (http.Header, int, error)
RequestJSONWithHeader sends any HTTP method with custom headers, optional JSON body, JSON-decodes the response, and returns the response headers.
This is the most general header-returning method; the *WithHeader convenience methods delegate to the same code path. See the *WithHeader documentation for the header return semantics.
func (*Client) RequestRaw ¶
func (c *Client) RequestRaw(ctx context.Context, method, url string, headers map[string]string, body any) ([]byte, int, error)
RequestRaw sends any HTTP method with custom headers and optional JSON body, returns raw response bytes.
func (*Client) RequestRawWithHeader ¶
func (c *Client) RequestRawWithHeader(ctx context.Context, method, url string, headers map[string]string, body any) (http.Header, []byte, int, error)
RequestRawWithHeader sends any HTTP method with custom headers and optional JSON body, returning the raw response body together with the response headers.
type Option ¶
type Option func(*Client)
Option configures a Client. Options are applied in the order given; WithHTTPClient replaces the underlying client wholesale, so list it before mutating options such as WithTimeout or WithCheckRedirect.
func WithBaseURL ¶
WithBaseURL sets a base URL that is prefixed to relative request URLs, so call sites pass only the path ("/v1/users") instead of rebuilding the full URL. A trailing slash on the base is trimmed. Request URLs that already contain a scheme ("https://...") bypass the base and are used as-is.
func WithCheckRedirect ¶
WithCheckRedirect sets the redirect policy used by the underlying http.Client. Pass http.ErrUseLastResponse from the function to return the latest 3xx response.
func WithDefaultHeaders ¶
WithDefaultHeaders sets headers applied to every request — typically Authorization, User-Agent, or a service-identity header. A per-call header with the same name overrides the default. The map is copied; changes the caller makes to it after New are not observed. Repeated use merges into the existing defaults.
func WithHTTPClient ¶
WithHTTPClient overrides the underlying http.Client.
The client struct is shallow-copied so that later mutating options (WithTimeout, WithCheckRedirect) modify the copy, never the caller's client. The Transport is shared by reference — its connection pool is reused. Isolation cuts both ways: changes the caller makes to hc after New (Timeout, Jar, CheckRedirect) are not observed by this Client either.
Place this option before mutating options: a WithTimeout listed earlier would be overwritten by the replacement client.
func WithMaxConnsPerHost ¶
WithMaxConnsPerHost caps the total connections (active plus idle) the transport opens to a single host. This is a protection knob, not a speed knob: it shields a fragile or rate-limited upstream from being overwhelmed by a burst of fan-out requests. Once the cap is reached, further requests to that host block until a connection frees. The default is 0 (unlimited).
Like WithMaxIdleConnsPerHost, the transport is cloned before the change, so a transport shared via WithHTTPClient is never mutated — at the cost that this client gets its own connection pool. A nil transport falls back to a clone of http.DefaultTransport. A custom non-*http.Transport RoundTripper is left untouched — configure its pool directly instead.
func WithMaxIdleConnsPerHost ¶
WithMaxIdleConnsPerHost sets the maximum idle (keep-alive) connections kept per host (default 10). Raise this when most traffic targets a single upstream, so high concurrency does not churn through new connections.
The transport is cloned before the change, so a transport shared via WithHTTPClient is never mutated — at the cost that this client gets its own connection pool. A nil transport falls back to a clone of http.DefaultTransport. A custom non-*http.Transport RoundTripper is left untouched — configure its pool directly instead.
func WithMaxResponseBytes ¶
WithMaxResponseBytes caps how many response body bytes httpc reads, guarding against memory exhaustion from oversized or malicious responses. The default is 10 MiB. Pass 0 (or a negative value) to disable the limit. When a body exceeds the cap, JSON/Raw methods return an error wrapping ErrResponseTooLarge.
The limit counts decompressed bytes (Go transparently decompresses gzip responses), which is what actually occupies memory — do not size it from Content-Length, which reflects the compressed payload.
func WithTimeout ¶
WithTimeout sets the HTTP client timeout (default 10s).
func WithTransport ¶
func WithTransport(t http.RoundTripper) Option
WithTransport overrides the HTTP transport.
func WithoutRedirect ¶
func WithoutRedirect() Option
WithoutRedirect disables automatic redirect following.