httpclient

package module
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 11 Imported by: 0

README

http-client-go

A production-ready HTTP client library for Go with retry, telemetry, lifecycle hooks, structured logging, and per-request options.

Installation

go get github.com/edaniel30/http-client-go

Quick Start

package main

import (
    "context"
    "fmt"

    httpclient "github.com/edaniel30/http-client-go"
)

func main() {
    client, err := httpclient.New(httpclient.DefaultConfig())
    if err != nil {
        panic(err)
    }
    defer client.Close()

    resp, err := client.Get(context.Background(), "https://api.example.com/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    fmt.Println("Status:", resp.StatusCode)
}

Configuration

Default Configuration
httpclient.DefaultConfig()
// Returns:
// - Timeout: 30s
// - Headers: empty
// - Logger: nil (disabled)
// - Retry: nil (disabled)
// - Transport: nil (Go default)
// - Hooks: nil
// - Telemetry: nil (disabled)
Configuration Options

All options use the functional options pattern:

Client Options
httpclient.WithTimeout(10 * time.Second)                          // Request timeout
httpclient.WithHeaders(map[string]string{"Authorization": "Bearer token"})  // Default headers
httpclient.WithLogger(myLogger)                                   // Structured logger
httpclient.WithTransport(&http.Transport{MaxIdleConns: 100})      // Connection pooling
httpclient.WithRetry(3, 100*time.Millisecond, 5*time.Second)      // Retry with backoff
httpclient.WithHooks(myHook)                                      // Lifecycle hooks
httpclient.WithTelemetry(&httpclient.TelemetryConfig{...})        // OpenTelemetry tracing

See detailed documentation for each feature:

  • Retry - Exponential backoff with jitter
  • Telemetry - OpenTelemetry distributed tracing
  • Hooks - Lifecycle callbacks for requests
  • Logging - Structured logging with obfuscation
  • Request Options - Per-request configuration

HTTP Methods

client.Get(ctx, url, opts...)
client.Post(ctx, url, contentType, body, opts...)
client.Put(ctx, url, contentType, body, opts...)
client.Patch(ctx, url, contentType, body, opts...)
client.Delete(ctx, url, opts...)
client.Head(ctx, url, opts...)
client.Options(ctx, url, opts...)
client.Do(ctx, req, opts...)  // Custom *http.Request

All methods accept optional per-request options (...RequestOption).

JSON Decoding

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

resp, _ := client.Get(ctx, "https://api.example.com/users/1")
user, err := httpclient.DecodeJSON[User](resp)

DecodeJSON closes the response body automatically.

Per-Request Options

Override client-level behavior for individual requests:

resp, _ := client.Get(ctx, url,
    httpclient.WithTraceID("trace-abc-123"),
    httpclient.WithSkipLog(),
    httpclient.WithTags(map[string]string{"operation": "charge"}),
    httpclient.WithObfuscatedHeaders("Authorization", "X-Api-Key"),
    httpclient.WithRequestHeaders(extraHeaders),
)

See Request Options Documentation for details.

Error Handling

The library provides typed errors for precise handling:

resp, err := client.Get(ctx, url)
if err != nil {
    var reqErr *httpclient.RequestError
    if errors.As(err, &reqErr) {
        fmt.Println(reqErr.Method, reqErr.URL, reqErr.Err)
    }

    if errors.Is(err, httpclient.ErrClientClosed) {
        fmt.Println("Client was closed")
    }
}

user, err := httpclient.DecodeJSON[User](resp)
if err != nil {
    var decodeErr *httpclient.ResponseDecodeError
    if errors.As(err, &decodeErr) {
        fmt.Println("Status:", decodeErr.StatusCode)
    }
}
Error Type When
*ConfigError Invalid configuration (timeout, retry, telemetry)
*RequestError HTTP request failure (network, timeout)
*ResponseDecodeError JSON decode failure
ErrClientClosed Client used after Close()

Lifecycle

client, err := httpclient.New(httpclient.DefaultConfig(), ...)
if err != nil {
    log.Fatal(err)
}
defer client.Close()  // Closes idle connections + flushes telemetry

Close() is idempotent and safe to call multiple times.

Full Example

client, err := httpclient.New(
    httpclient.DefaultConfig(),
    httpclient.WithTimeout(10*time.Second),
    httpclient.WithHeaders(map[string]string{
        "Authorization": "Bearer " + token,
    }),
    httpclient.WithRetry(3, 100*time.Millisecond, 5*time.Second),
    httpclient.WithLogger(myLogger),
    httpclient.WithTelemetry(&httpclient.TelemetryConfig{
        ServiceName:  "payment-service",
        Version:      "1.0.0",
        Environment:  "production",
        OTLPEndpoint: "localhost:4318",
        SampleAll:    false,
    }),
)
defer client.Close()

resp, err := client.Post(
    ctx,
    "https://api.example.com/payments",
    "application/json",
    strings.NewReader(`{"amount": 100}`),
    httpclient.WithTraceID("tx-abc-123"),
    httpclient.WithObfuscatedHeaders("Authorization"),
    httpclient.WithTags(map[string]string{"provider": "stripe"}),
)

Dependencies

Required
Optional (only with telemetry enabled)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrClientClosed = errors.New("http client is closed")

Functions

func DecodeJSON

func DecodeJSON[T any](resp *http.Response) (T, error)

Types

type Client

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

func New

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

func (*Client) Close

func (c *Client) Close()

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, url string, opts ...RequestOption) (*http.Response, error)

func (*Client) Do

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

func (*Client) Get

func (c *Client) Get(ctx context.Context, url string, opts ...RequestOption) (*http.Response, error)

func (*Client) Head

func (c *Client) Head(ctx context.Context, url string, opts ...RequestOption) (*http.Response, error)

func (*Client) Options

func (c *Client) Options(ctx context.Context, url string, opts ...RequestOption) (*http.Response, error)

func (*Client) Patch

func (c *Client) Patch(ctx context.Context, url string, contentType string, body io.Reader, opts ...RequestOption) (*http.Response, error)

func (*Client) Post

func (c *Client) Post(ctx context.Context, url string, contentType string, body io.Reader, opts ...RequestOption) (*http.Response, error)

func (*Client) Put

func (c *Client) Put(ctx context.Context, url string, contentType string, body io.Reader, opts ...RequestOption) (*http.Response, error)

type Config

type Config struct {
	Timeout   time.Duration
	Headers   map[string]string
	Logger    Logger
	Retry     *RetryConfig
	Transport *http.Transport
	Hooks     []Hook
	Telemetry *TelemetryConfig
}

func DefaultConfig

func DefaultConfig() *Config

type ConfigError

type ConfigError struct {
	Field   string
	Message string
}

ConfigError represents a validation error in the client configuration.

func NewConfigError

func NewConfigError(field, message string) *ConfigError

func (*ConfigError) Error

func (e *ConfigError) Error() string

type Hook

type Hook interface {
	// OnRequestStart is called before the request is sent.
	OnRequestStart(req *http.Request)

	// OnRequestEnd is called after a successful response is received.
	OnRequestEnd(req *http.Request, resp *http.Response)

	// OnError is called when the request fails with an error.
	OnError(req *http.Request, err error)
}

Hook defines lifecycle callbacks for HTTP requests. All methods are optional — implement only the ones you need.

type Logger

type Logger interface {
	// Info logs an informational message with optional fields
	Info(ctx context.Context, msg string, fields map[string]any)

	// Error logs an error message with optional fields
	Error(ctx context.Context, msg string, fields map[string]any)

	// Warn logs a warning message with optional fields
	Warn(ctx context.Context, msg string, fields map[string]any)

	// Debug logs a debug message with optional fields
	Debug(ctx context.Context, msg string, fields map[string]any)

	// Close closes the logger and flushes any pending logs.
	// Note: This should be called by the logger creator (usually in main()),
	// not by the platform. Use defer logger.Close() after creating the logger.
	// Returns an error if the logger fails to close or flush properly.
	Close() error
}

Logger is the interface that any logger implementation must satisfy. This allows the client to be agnostic about the logging implementation.

The client does NOT call Close(). The caller who creates the logger is responsible for its lifecycle.

Example adapter for loki-logger-go:

// loki.Logger already satisfies this interface, pass it directly:
httpClient, _ := httpclient.New(
    httpclient.DefaultConfig(),
    httpclient.WithLogger(lokiLogger),
)

type Option

type Option func(*Config)

func WithHeaders

func WithHeaders(h map[string]string) Option

func WithHooks

func WithHooks(hooks ...Hook) Option

WithHooks adds lifecycle hooks that are called on request start, end, and error.

func WithLogger

func WithLogger(l Logger) Option

func WithRetry

func WithRetry(maxRetries int, minBackoff, maxBackoff time.Duration) Option

func WithTelemetry

func WithTelemetry(t *TelemetryConfig) Option

WithTelemetry enables OpenTelemetry distributed tracing. The client initializes OTLP export and creates spans for every HTTP request. Pass nil to disable telemetry.

func WithTimeout

func WithTimeout(d time.Duration) Option

func WithTransport

func WithTransport(t *http.Transport) Option

WithTransport sets a custom HTTP transport for connection pooling and TLS configuration.

type RequestError

type RequestError struct {
	Method string
	URL    string
	Err    error
}

RequestError represents a failure executing an HTTP request.

func (*RequestError) Error

func (e *RequestError) Error() string

func (*RequestError) Unwrap

func (e *RequestError) Unwrap() error

type RequestOption

type RequestOption func(*requestOpts)

RequestOption configures per-request behavior, overriding client-level defaults.

func WithObfuscatedHeaders

func WithObfuscatedHeaders(headers ...string) RequestOption

WithObfuscatedHeaders specifies header names whose values should be masked in logs. The headers are sent with their original values but logged as "********".

func WithRequestHeaders

func WithRequestHeaders(headers http.Header) RequestOption

WithRequestHeaders adds extra headers only for this specific request.

func WithSkipLog

func WithSkipLog() RequestOption

WithSkipLog disables logging for this specific request.

func WithTags

func WithTags(tags map[string]string) RequestOption

WithTags adds custom key-value tags to the log entries for this request.

func WithTraceID

func WithTraceID(traceID string) RequestOption

WithTraceID adds an X-Trace-Id header to the request for distributed tracing.

type ResponseDecodeError

type ResponseDecodeError struct {
	StatusCode int
	Err        error
}

ResponseDecodeError represents a failure decoding an HTTP response body.

func (*ResponseDecodeError) Error

func (e *ResponseDecodeError) Error() string

func (*ResponseDecodeError) Unwrap

func (e *ResponseDecodeError) Unwrap() error

type RetryConfig

type RetryConfig struct {
	MaxRetries int
	MinBackoff time.Duration
	MaxBackoff time.Duration
}

type TelemetryConfig

type TelemetryConfig struct {
	ServiceName  string
	Version      string
	Environment  string
	OTLPEndpoint string
	SampleAll    bool
}

TelemetryConfig configures OpenTelemetry distributed tracing. When provided, the client automatically creates spans for every HTTP request and exports them via OTLP to the configured endpoint (Datadog Agent, Jaeger, etc).

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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