Skip to content

A comprehensive Go package for building and executing HTTP requests with advanced features

License

Notifications You must be signed in to change notification settings

slashdevops/httpx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

httpx

main branch GitHub go.mod Go version Go Reference Go Report Card license Release

A comprehensive Go package for building and executing HTTP requests with advanced features.

🚀 Zero Dependencies - Built entirely using the Go standard library for maximum reliability, security, and minimal maintenance overhead. See go.mod

Key Features

  • 🔨 Fluent Request Builder - Chainable API for constructing HTTP requests
  • 🔄 Automatic Retry Logic - Configurable retry strategies with exponential backoff
  • 🎯 Type-Safe Generic Client - Go generics for type-safe HTTP responses
  • Input Validation - Comprehensive validation with error accumulation
  • 🔐 Authentication Support - Built-in Basic and Bearer token authentication
  • 🌐 Proxy Support - HTTP/HTTPS proxy configuration with authentication (supports corporate proxies, authenticated proxies, and custom ports)
  • 📝 Optional Logging - slog integration for observability (disabled by default)
  • 📦 Zero External Dependencies - Only Go standard library, no third-party packages

Table of Contents

Installation

Requirements: Go 1.22 or higher

go get github.com/slashdevops/httpx

Upgrade

To upgrade to the latest version, run:

go get -u github.com/slashdevops/httpx

Quick Start

Simple GET Request

import "github.com/slashdevops/httpx"

// Build and execute a simple GET request
req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/users/123").
    WithHeader("Accept", "application/json").
    Build()

if err != nil {
    log.Fatal(err)
}

// Use with standard http.Client
resp, err := http.DefaultClient.Do(req)

Type-Safe Requests with Generic Client

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Create a typed client with configuration
client := httpx.NewGenericClient[User](
    httpx.WithTimeout[User](10 * time.Second),
    httpx.WithMaxRetries[User](3),
    httpx.WithRetryStrategy[User](httpx.ExponentialBackoffStrategy),
)

// Execute typed request
response, err := client.Get("https://api.example.com/users/123")
if err != nil {
    log.Fatal(err)
}

// response.Data is strongly typed as User
fmt.Printf("User: %s (%s)\n", response.Data.Name, response.Data.Email)

Request with Retry Logic

// Create client with retry logic
retryClient := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.ExponentialBackoffStrategy).
    WithRetryBaseDelay(500 * time.Millisecond).
    Build()

// Use with generic client
client := httpx.NewGenericClient[User](
    httpx.WithHTTPClient[User](retryClient),
    httpx.)

response, err := client.Get("/users/123")

Features

Request Builder

The RequestBuilder provides a fluent, chainable API for constructing HTTP requests with comprehensive validation.

Key Features

  • ✅ HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, CONNECT
  • ✅ Convenience methods for all standard HTTP methods (WithMethodGET, WithMethodPOST, WithMethodPUT, WithMethodDELETE, WithMethodPATCH, WithMethodHEAD, WithMethodOPTIONS, WithMethodTRACE, WithMethodCONNECT)
  • ✅ Query parameters with automatic URL encoding
  • ✅ Custom headers with validation
  • ✅ Authentication (Basic Auth, Bearer Token)
  • ✅ Multiple body formats (JSON, string, bytes, io.Reader)
  • ✅ Context support for timeouts and cancellation
  • ✅ Input validation with error accumulation
  • ✅ Comprehensive error messages

Usage Example

req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodPOST().
    WithPath("/users").
    WithQueryParam("notify", "true").
    WithHeader("Content-Type", "application/json").
    WithHeader("X-Request-ID", "unique-id-123").
    WithBearerAuth("your-token-here").
    WithJSONBody(map[string]string{
        "name":  "John Doe",
        "email": "john@example.com",
    }).
    Build()

if err != nil {
    // Handle validation errors
    log.Fatal(err)
}

Validation Features

The RequestBuilder validates inputs and accumulates errors:

builder := httpx.NewRequestBuilder("https://api.example.com")
builder.HTTPMethod("")           // Error: empty method
builder.WithHeader("", "value")      // Error: empty header key
builder.WithQueryParam("key=", "val") // Error: invalid character in key

// Check for errors before building
if builder.HasErrors() {
    for _, err := range builder.GetErrors() {
        log.Printf("Validation error: %v", err)
    }
}

// Or let Build() report all errors
req, err := builder.Build()
if err != nil {
    // err contains all accumulated validation errors
    log.Fatal(err)
}

Reset and Reuse

builder := httpx.NewRequestBuilder("https://api.example.com")

// Use builder
req1, _ := builder.WithWithMethodGET().WithPath("/users").Build()

// Reset and reuse
builder.Reset()
req2, _ := builder.WithWithMethodPOST().WithPath("/posts").Build()

Generic HTTP Client

The GenericClient provides type-safe HTTP requests with automatic JSON marshaling and unmarshaling using Go generics.

Key Features

  • 🎯 Type-safe responses with automatic JSON unmarshaling
  • 🔄 Convenience methods: Get, Post, Put, Delete, Patch
  • 🔌 Execute method for use with RequestBuilder
  • 📦 ExecuteRaw for non-JSON responses
  • 🌐 Base URL resolution for relative paths
  • 📋 Default headers applied to all requests
  • ❌ Structured error responses
  • 🔁 Full integration with retry logic

Basic Usage

type Post struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Body   string `json:"body"`
    UserID int    `json:"userId"`
}

client := httpx.NewGenericClient[Post](
    httpx.WithTimeout[Post](10 * time.Second),
    httpx.WithMaxRetries[Post](3),
    httpx.WithRetryStrategy[Post](httpx.ExponentialBackoffStrategy),
)

// GET request
response, err := client.Get("https://api.example.com/posts/1")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Title: %s\n", response.Data.Title)

// POST request
newPost := Post{Title: "New Post", Body: "Content", UserID: 1}
postData, _ := json.Marshal(newPost)
response, err = client.Post("https://api.example.com/posts", bytes.NewReader(postData))

With RequestBuilder

Combine GenericClient with RequestBuilder for maximum flexibility:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

client := httpx.NewGenericClient[User](
    httpx.WithTimeout[User](15 * time.Second),
    httpx.WithMaxRetries[User](3),
)

// Build complex request
req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodPOST().
    WithPath("/users").
    WithContentType("application/json").
    WithHeader("X-Request-ID", "unique-123").
    WithJSONBody(User{Name: "Jane", Email: "jane@example.com"}).
    Build()

if err != nil {
    log.Fatal(err)
}

// Execute with type safety
response, err := client.Execute(req)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Created user ID: %d\n", response.Data.ID)

Error Handling

The generic client returns structured errors:

response, err := client.Get("/users/999999")
if err != nil {
    // Check if it's an API error
    if apiErr, ok := err.(*httpx.ErrorResponse); ok {
        fmt.Printf("API Error %d: %s\n", apiErr.StatusCode, apiErr.Message)
        // StatusCode: 404
        // Message: "User not found"
    } else {
        // Network error, parsing error, etc.
        log.Printf("Request failed: %v\n", err)
    }
    return
}

Multiple Typed Clients

Use different clients for different response types:

type User struct { /* ... */ }
type Post struct { /* ... */ }

userClient := httpx.NewGenericClient[User](
    httpx.WithTimeout[User](10 * time.Second),
)

postClient := httpx.NewGenericClient[Post](
    httpx.WithTimeout[Post](10 * time.Second),
)

// Fetch user
userResp, _ := userClient.Get("/users/1")

// Fetch user's posts
postsResp, _ := postClient.Get(fmt.Sprintf("/users/%d/posts", userResp.Data.ID))

Retry Logic

The package provides transparent retry logic with configurable strategies.

Retry Strategies

Exponential Backoff (Recommended)

Doubles the wait time between retries:

client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.ExponentialBackoffStrategy).
    WithRetryBaseDelay(500 * time.Millisecond).
    WithRetryMaxDelay(10 * time.Second).
    Build()

Wait times: 500ms → 1s → 2s → 4s (capped at maxDelay)

Fixed Delay

Waits a constant duration between retries:

client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.FixedDelayStrategy).
    WithRetryBaseDelay(1 * time.Second).
    Build()

Wait times: 1s → 1s → 1s

Jitter Backoff

Adds randomization to exponential backoff to prevent thundering herd:

client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.JitterBackoffStrategy).
    WithRetryBaseDelay(500 * time.Millisecond).
    WithRetryMaxDelay(10 * time.Second).
    Build()

Wait times: Random between 0-500ms → 0-1s → 0-2s

What Gets Retried?

The retry logic automatically retries:

  • Network errors (connection failures, timeouts)
  • HTTP 5xx server errors (500-599)
  • HTTP 429 (Too Many Requests)

Does NOT retry:

  • HTTP 4xx client errors (except 429)
  • HTTP 2xx/3xx successful responses
  • Requests without GetBody (non-replayable requests)

Retry with Generic Client

// Create retry client
retryClient := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.ExponentialBackoffStrategy).
    Build()

// Use with generic client
client := httpx.NewGenericClient[User](
    httpx.WithHTTPClient[User](retryClient),
    httpx.)

// Requests automatically retry on failure
response, err := client.Get("/users/1")

Client Builder

The ClientBuilder provides fine-grained control over HTTP client configuration.

Configuration Options

client := httpx.NewClientBuilder().
    // Timeouts
    WithTimeout(30 * time.Second).
    WithIdleConnTimeout(90 * time.Second).
    WithTLSHandshakeTimeout(10 * time.Second).
    WithExpectContinueTimeout(1 * time.Second).

    // Connection pooling
    WithMaxIdleConns(100).
    WithMaxIdleConnsPerHost(10).
    WithDisableKeepAlive(false).

    // Retry configuration
    WithMaxRetries(3).
    WithRetryStrategy(httpx.ExponentialBackoffStrategy).
    WithRetryBaseDelay(500 * time.Millisecond).
    WithRetryMaxDelay(10 * time.Second).

    Build()

Default Values

Setting Default Valid Range
Timeout 5s 1s - 30s
MaxRetries 3 1 - 10
RetryBaseDelay 500ms 300ms - 5s
RetryMaxDelay 10s 300ms - 120s
MaxIdleConns 100 1 - 200
IdleConnTimeout 90s 1s - 120s
TLSHandshakeTimeout 10s 1s - 15s

The builder validates all settings and uses defaults for out-of-range values.

Proxy Configuration

The httpx package provides comprehensive HTTP/HTTPS proxy support across all client types. Configure proxies to route your requests through corporate firewalls, load balancers, or testing proxies.

Key Features

  • ✅ HTTP and HTTPS proxy support
  • 🔐 Proxy authentication (username/password)
  • 🔄 Works with retry logic
  • 🎯 Compatible with all client types
  • 🌐 Full URL or host:port formats
  • 📝 Graceful fallback on invalid URLs

Basic Usage

With ClientBuilder
// HTTP proxy
client := httpx.NewClientBuilder().
    WithProxy("http://proxy.example.com:8080").
    WithTimeout(10 * time.Second).
    Build()

// HTTPS proxy
client := httpx.NewClientBuilder().
    WithProxy("https://secure-proxy.example.com:3128").
    Build()
With GenericClient
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

client := httpx.NewGenericClient[User](
    httpx.WithProxy[User]("http://proxy.example.com:8080"),
    httpx.WithTimeout[User](10*time.Second),
    httpx.WithMaxRetries[User](3),
)

response, err := client.Get("https://api.example.com/users/1")
With Retry Client
client := httpx.NewHTTPRetryClient(
    httpx.WithProxyRetry("http://proxy.example.com:8080"),
    httpx.WithMaxRetriesRetry(5),
    httpx.WithRetryStrategyRetry(
        httpx.ExponentialBackoff(500*time.Millisecond, 30*time.Second),
    ),
)

Proxy Authentication

Include credentials directly in the proxy URL:

client := httpx.NewClientBuilder().
    WithProxy("http://username:password@proxy.example.com:8080").
    Build()

Security Note: For production, consider using environment variables or secret management:

proxyURL := fmt.Sprintf("http://%s:%s@%s:%s",
    os.Getenv("PROXY_USER"),
    os.Getenv("PROXY_PASS"),
    os.Getenv("PROXY_HOST"),
    os.Getenv("PROXY_PORT"),
)

client := httpx.NewClientBuilder().
    WithProxy(proxyURL).
    Build()

Common Proxy Ports

  • HTTP Proxy: 8080, 3128, 8888
  • HTTPS Proxy: 3128, 8443
  • Squid: 3128 (most common)
  • Corporate Proxies: 8080, 80

Disable Proxy

Override environment proxy settings by passing an empty string:

// Disable proxy (ignore HTTP_PROXY environment variable)
client := httpx.NewClientBuilder().
    WithProxy("").
    Build()

Complete Example

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/slashdevops/httpx"
)

type APIResponse struct {
    Message string `json:"message"`
    Status  string `json:"status"`
}

func main() {
    // Configure client with proxy and full options
    client := httpx.NewGenericClient[APIResponse](
        httpx.WithProxy[APIResponse]("http://proxy.example.com:8080"),
        httpx.WithTimeout[APIResponse](15*time.Second),
        httpx.WithMaxRetries[APIResponse](5),
        httpx.WithRetryStrategy[APIResponse](httpx.JitterBackoffStrategy),
        httpx.WithRetryBaseDelay[APIResponse](500*time.Millisecond),
        httpx.WithRetryMaxDelay[APIResponse](30*time.Second),
    )

    // Build request with authentication
    req, err := httpx.NewRequestBuilder("https://api.example.com").
        WithMethodGET().
        WithPath("/data").
        WithBearerAuth("your-token-here").
        WithHeader("Accept", "application/json").
        Build()

    if err != nil {
        log.Fatal(err)
    }

    // Execute through proxy
    response, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Response: %s\n", response.Data.Message)
}

Error Handling

The library gracefully handles proxy configuration errors:

client := httpx.NewClientBuilder().
    WithProxy("://invalid-url").  // Invalid URL
    WithLogger(logger).            // Optional: log warnings
    Build()

// Client builds successfully, but proxy is not configured
// Warning logged if logger is provided

Logging

The httpx package supports optional logging using Go's standard log/slog package. Logging is disabled by default to maintain clean, silent HTTP operations. Enable it when you need observability into retries, errors, and other HTTP client operations.

Quick Start

Basic Usage
import (
    "log/slog"
    "os"

    "github.com/slashdevops/httpx"
)

// Create a logger
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelWarn,
}))

// Use with ClientBuilder
client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithLogger(logger).  // Enable logging
    Build()
With Generic Client
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))

client := httpx.NewGenericClient[User](
    httpx.WithMaxRetries[User](3),
    httpx.WithLogger[User](logger),
)
With NewHTTPRetryClient
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

client := httpx.NewHTTPRetryClient(
    httpx.WithMaxRetriesRetry(3),
    httpx.WithRetryStrategyRetry(httpx.ExponentialBackoff(500*time.Millisecond, 10*time.Second)),
    httpx.WithLoggerRetry(logger),
)

What Gets Logged

Retry Attempts (Warn Level)

When a request fails and is being retried:

time=2026-01-17T21:00:00.000+00:00 level=WARN msg="HTTP request returned server error, retrying" attempt=1 max_retries=3 delay=500ms status_code=500 url=https://api.example.com/users method=GET

Attributes logged:

  • attempt: Current retry attempt number (1-indexed)
  • max_retries: Maximum number of retries configured
  • delay: How long the client will wait before retrying
  • status_code: HTTP status code (for server errors) OR
  • error: Error message (for network/connection errors)
  • url: Full request URL
  • method: HTTP method (GET, POST, etc.)
All Retries Failed (Error Level)

When all retry attempts are exhausted:

time=2026-01-17T21:00:00.500+00:00 level=ERROR msg="All retry attempts failed" attempts=4 status_code=503 url=https://api.example.com/users method=GET

Attributes logged:

  • attempts: Total number of attempts made (including initial request)
  • status_code OR error: Final failure reason
  • url: Full request URL
  • method: HTTP method

Logger Configuration

Log Levels

Choose the appropriate log level based on your needs:

// Only log final failures (recommended for production)
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelError,
}))

// Log all retry attempts (useful for debugging)
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelWarn,
}))

// Log everything including debug info from other packages
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
Output Formats
Text Format (Development)

Best for human readability during development:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelWarn,
}))

Output:

time=2026-01-17T21:00:00.000+00:00 level=WARN msg="HTTP request returned server error, retrying" attempt=1 max_retries=3 delay=500ms status_code=500
JSON Format (Production)

Best for structured logging and log aggregation:

logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelError,
}))

Output:

{"time":"2026-01-17T21:00:00.000Z","level":"ERROR","msg":"All retry attempts failed","attempts":4,"status_code":503,"url":"https://api.example.com/users","method":"GET"}
Writing to Files
logFile, err := os.OpenFile("http.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
    log.Fatal(err)
}
defer logFile.Close()

logger := slog.New(slog.NewJSONHandler(logFile, &slog.HandlerOptions{
    Level: slog.LevelWarn,
}))

Logging Best Practices

  1. Default to No Logging: Keep logging disabled in production unless actively troubleshooting:

    // Production - no logging (default)
    client := httpx.NewClientBuilder().
        WithMaxRetries(3).
        Build()  // No WithLogger() call = no logging
  2. Use Structured Logging in Production: JSON format is machine-readable and works well with log aggregators:

    logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: slog.LevelError,  // Only final failures
    }))
  3. Enable for Specific Troubleshooting: Turn on logging temporarily when investigating issues:

    // Temporarily enable for debugging
    var logger *slog.Logger
    if os.Getenv("DEBUG_HTTP") != "" {
        logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
            Level: slog.LevelWarn,
        }))
    }
    
    client := httpx.NewClientBuilder().
        WithMaxRetries(3).
        WithLogger(logger).  // Will be nil if not debugging
        Build()
  4. Add Context with Attributes: Enhance logs with additional context:

    // Create logger with service context
    logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)).
        With("service", "api-client").
        With("version", "1.0.0")
    
    client := httpx.NewClientBuilder().
        WithLogger(logger).
        Build()
  5. Different Loggers for Different Clients: Use separate loggers for different clients to distinguish traffic:

    // User service client
    userLogger := slog.New(slog.NewJSONHandler(os.Stderr, nil)).
        With("client", "user-service")
    userClient := httpx.NewClientBuilder().
        WithLogger(userLogger).
        Build()
    
    // Payment service client
    paymentLogger := slog.New(slog.NewJSONHandler(os.Stderr, nil)).
        With("client", "payment-service")
    paymentClient := httpx.NewClientBuilder().
        WithLogger(paymentLogger).
        Build()

Performance Considerations

  • Minimal Overhead: When logging is disabled (logger is nil), the overhead is just a simple nil check
  • No Allocations: Log statements use slog's efficient attribute system
  • Deferred Work: The logger only formats messages if the log level is enabled

Disabling Logging

Simply pass nil or omit the logger:

// Explicitly pass nil
client := httpx.NewClientBuilder().
    WithLogger(nil).  // No logging
    Build()

// Or just don't call WithLogger
client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    Build()  // No logging (default)

Migration Guide

If you have existing code without logging, no changes are needed. The feature is fully backward compatible:

// Old code - still works, no logging
client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    Build()

// New code - add logging when needed
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithLogger(logger).  // Just add this line
    Build()

Logging Examples

Example 1: Development Debugging
package main

import (
    "log/slog"
    "os"
    "time"

    "github.com/slashdevops/httpx"
)

func main() {
    // Text output with warn level for debugging
    logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelWarn,
    }))

    client := httpx.NewClientBuilder().
        WithMaxRetries(3).
        WithRetryBaseDelay(500 * time.Millisecond).
        WithLogger(logger).
        Build()

    // You'll see retry attempts in the console
    resp, err := client.Get("https://api.example.com/flaky-endpoint")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
}
Example 2: Production Monitoring
package main

import (
    "log/slog"
    "os"

    "github.com/slashdevops/httpx"
)

func main() {
    // JSON output, only errors, to stderr
    logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: slog.LevelError,
    })).With(
        "service", "payment-processor",
        "environment", "production",
    )

    client := httpx.NewClientBuilder().
        WithMaxRetries(3).
        WithLogger(logger).
        Build()

    // Only final failures will be logged
    resp, err := client.Get("https://payment-api.example.com/status")
    // ...
}
Example 3: Conditional Logging
package main

import (
    "log/slog"
    "os"

    "github.com/slashdevops/httpx"
)

func createClient() *http.Client {
    var logger *slog.Logger

    // Only enable logging if DEBUG environment variable is set
    if os.Getenv("DEBUG") != "" {
        logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
            Level: slog.LevelDebug,
        }))
    }

    return httpx.NewClientBuilder().
        WithMaxRetries(3).
        WithLogger(logger).  // Will be nil in production
        Build()
}

Troubleshooting

Not Seeing Any Logs?
  1. Check logger level: Make sure the level is set to at least LevelWarn:

    logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelWarn,  // Not Info or Debug
    }))
  2. Verify logger is passed: Make sure you called WithLogger():

    client := httpx.NewClientBuilder().
        WithLogger(logger).  // Don't forget this!
        Build()
  3. Check if retries are happening: Logs only appear when requests fail and retry. Successful first attempts don't log.

Too Many Logs?
  1. Increase log level to LevelError to only see final failures
  2. Disable logging in production environments where retry behavior is well understood
  3. Use sampling if your log aggregation system supports it

Logging Summary

The logging feature in httpx provides:

  • Optional - Disabled by default, zero overhead when not in use
  • Standard - Uses Go's log/slog package
  • Flexible - Configurable output format, level, and destination
  • Informative - Rich attributes for debugging and monitoring
  • Backward Compatible - Existing code works without changes

Enable it when you need visibility, keep it off for clean, silent operations.

Examples

Complete Example: CRUD Operations

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/slashdevops/httpx"
)

type Todo struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
    UserID    int    `json:"userId"`
}

func main() {
    // Create retry client
    retryClient := httpx.NewClientBuilder().
        WithMaxRetries(3).
        WithRetryStrategy(httpx.ExponentialBackoffStrategy).
        WithTimeout(10 * time.Second).
        Build()

    // Create typed client
    client := httpx.NewGenericClient[Todo](
        httpx.WithHTTPClient[Todo](retryClient),
        httpx.        httpx.    )

    // GET - Read
    fmt.Println("Fetching todo...")
    todo, err := client.Get("/todos/1")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Todo: %s (completed: %v)\n", todo.Data.Title, todo.Data.Completed)

    // POST - Create
    fmt.Println("\nCreating new todo...")
    newTodo := Todo{
        Title:     "Learn httputils",
        Completed: false,
        UserID:    1,
    }

    req, _ := httpx.NewRequestBuilder("https://jsonplaceholder.typicode.com").
        WithMethodPOST().
        WithPath("/todos").
        WithContentType("application/json").
        WithJSONBody(newTodo).
        Build()

    created, err := client.Execute(req)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created todo ID: %d\n", created.Data.ID)

    // PUT - Update
    fmt.Println("\nUpdating todo...")
    updateTodo := created.Data
    updateTodo.Completed = true

    req, _ = httpx.NewRequestBuilder("https://jsonplaceholder.typicode.com").
        WithMethodPUT().
        WithPath(fmt.Sprintf("/todos/%d", updateTodo.ID)).
        WithContentType("application/json").
        WithJSONBody(updateTodo).
        Build()

    updated, err := client.Execute(req)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Updated: completed = %v\n", updated.Data.Completed)

    // DELETE
    fmt.Println("\nDeleting todo...")
    deleteResp, err := client.Delete(fmt.Sprintf("/todos/%d", updateTodo.ID))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Deleted (status: %d)\n", deleteResp.StatusCode)
}

Authentication Example

// Basic Authentication
req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/protected/resource").
    WithBasicAuth("username", "password").
    Build()

// Bearer Token Authentication
req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/protected/resource").
    WithBearerAuth("your-jwt-token").
    Build()

// With Generic Client
client := httpx.NewGenericClient[Resource](
    httpx.    httpx.)

Context and Timeout

// Request with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/slow-endpoint").
    Context(ctx).
    Build()

// Request with cancellation
ctx, cancel := context.WithCancel(context.Background())

go func() {
    time.Sleep(2 * time.Second)
    cancel() // Cancel after 2 seconds
}()

req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/endpoint").
    Context(ctx).
    Build()

Custom Headers and Query Parameters

req, err := httpx.NewRequestBuilder("https://api.example.com").
    WithMethodGET().
    WithPath("/search").
    WithQueryParam("q", "golang").
    WithQueryParam("sort", "relevance").
    WithQueryParam("limit", "10").
    WithHeader("Accept", "application/json").
    WithHeader("Accept-Language", "en-US").
    WithHeader("X-Request-ID", generateRequestID()).
    WithHeader("X-Correlation-ID", getCorrelationID()).
    WithUserAgent("MyApp/1.0 (Go)").
    Build()

API Reference

RequestBuilder

Constructor

  • NewRequestBuilder(baseURL string) *RequestBuilder

HTTP Methods

  • WithMethodGET() *RequestBuilder
  • WithMethodPOST() *RequestBuilder
  • WithMethodPUT() *RequestBuilder
  • WithMethodDELETE() *RequestBuilder
  • WithMethodPATCH() *RequestBuilder
  • WithMethodHEAD() *RequestBuilder
  • WithMethodOPTIONS() *RequestBuilder
  • WithMethodTRACE() *RequestBuilder
  • WithMethodCONNECT() *RequestBuilder
  • WithMethod(method string) *RequestBuilder - Custom HTTP method with validation

URL and Parameters

  • WithPath(path string) *RequestBuilder - Set URL path
  • WithQueryParam(key, value string) *RequestBuilder - Add single query parameter
  • QueryParams(params map[string]string) *RequestBuilder - Add multiple query parameters

Headers

  • WithHeader(key, value string) *RequestBuilder - Set single header
  • Headers(headers map[string]string) *RequestBuilder - Set multiple headers
  • WithContentType(contentType string) *RequestBuilder - Set Content-Type header
  • WithAccept(accept string) *RequestBuilder - Set Accept header
  • WithUserAgent(userAgent string) *RequestBuilder - Set User-Agent header

Authentication

  • WithBasicAuth(username, password string) *RequestBuilder - Set Basic authentication
  • WithBearerAuth(token string) *RequestBuilder - Set Bearer token authentication

Body

  • WithJSONBody(body any) *RequestBuilder - Set JSON body (auto-marshals)
  • RawBody(body io.Reader) *RequestBuilder - Set raw body
  • WithStringBody(body string) *RequestBuilder - Set string body
  • BytesBody(body []byte) *RequestBuilder - Set bytes body

Other

  • Context(ctx context.Context) *RequestBuilder - Set request context
  • Build() (*http.Request, error) - Build and validate request

Error Handling

  • HasErrors() bool - Check if there are validation errors
  • GetErrors() []error - Get all validation errors
  • Reset() *RequestBuilder - Reset builder state

GenericClient[T any]

Constructor

  • NewGenericClient[T any](options ...GenericClientOption[T]) *GenericClient[T]

Options

  • WithHTTPClient[T any](httpClient HTTPClient) GenericClientOption[T] - Use a pre-configured HTTP client (takes precedence)
  • WithTimeout[T any](timeout time.Duration) GenericClientOption[T] - Set request timeout
  • WithMaxRetries[T any](maxRetries int) GenericClientOption[T] - Set maximum retry attempts
  • WithRetryStrategy[T any](strategy Strategy) GenericClientOption[T] - Set retry strategy (fixed, jitter, exponential)
  • WithRetryStrategyAsString[T any](strategy string) GenericClientOption[T] - Set retry strategy from string
  • WithRetryBaseDelay[T any](baseDelay time.Duration) GenericClientOption[T] - Set base delay for retry strategies
  • WithRetryMaxDelay[T any](maxDelay time.Duration) GenericClientOption[T] - Set maximum delay for retry strategies
  • WithMaxIdleConns[T any](maxIdleConns int) GenericClientOption[T] - Set maximum idle connections
  • WithIdleConnTimeout[T any](idleConnTimeout time.Duration) GenericClientOption[T] - Set idle connection timeout
  • WithTLSHandshakeTimeout[T any](tlsHandshakeTimeout time.Duration) GenericClientOption[T] - Set TLS handshake timeout
  • WithExpectContinueTimeout[T any](expectContinueTimeout time.Duration) GenericClientOption[T] - Set expect continue timeout
  • WithMaxIdleConnsPerHost[T any](maxIdleConnsPerHost int) GenericClientOption[T] - Set maximum idle connections per host
  • WithDisableKeepAlive[T any](disableKeepAlive bool) GenericClientOption[T] - Disable HTTP keep-alive

Methods

  • Execute(req *http.Request) (*Response[T], error) - Execute request with type safety
  • ExecuteRaw(req *http.Request) (*http.Response, error) - Execute and return raw response
  • Do(req *http.Request) (*Response[T], error) - Alias for Execute
  • Get(url string) (*Response[T], error) - Execute GET request
  • Post(url string, body io.Reader) (*Response[T], error) - Execute POST request
  • Put(url string, body io.Reader) (*Response[T], error) - Execute PUT request
  • Delete(url string) (*Response[T], error) - Execute DELETE request
  • Patch(url string, body io.Reader) (*Response[T], error) - Execute PATCH request
  • GetBaseURL() string - Get configured base URL
  • GetDefaultHeaders() map[string]string - Get configured headers

ClientBuilder

Constructor

  • NewClientBuilder() *ClientBuilder

Configuration Methods

  • WithTimeout(timeout time.Duration) *ClientBuilder
  • WithMaxRetries(maxRetries int) *ClientBuilder
  • WithRetryStrategy(strategy Strategy) *ClientBuilder
  • WithRetryBaseDelay(baseDelay time.Duration) *ClientBuilder
  • WithRetryMaxDelay(maxDelay time.Duration) *ClientBuilder
  • WithMaxIdleConns(maxIdleConns int) *ClientBuilder
  • WithMaxIdleConnsPerHost(maxIdleConnsPerHost int) *ClientBuilder
  • WithIdleConnTimeout(idleConnTimeout time.Duration) *ClientBuilder
  • WithTLSHandshakeTimeout(tlsHandshakeTimeout time.Duration) *ClientBuilder
  • WithExpectContinueTimeout(expectContinueTimeout time.Duration) *ClientBuilder
  • WithDisableKeepAlive(disableKeepAlive bool) *ClientBuilder
  • Build() *http.Client - Build configured client

Retry Strategies

  • ExponentialBackoff(base, maxDelay time.Duration) RetryStrategy
  • FixedDelay(delay time.Duration) RetryStrategy
  • JitterBackoff(base, maxDelay time.Duration) RetryStrategy

Types

Response[T any]

type Response[T any] struct {
    Data       T           // Parsed response data
    StatusCode int         // HTTP status code
    Headers    http.Header // Response headers
    RawBody    []byte      // Raw response body
}

ErrorResponse

type ErrorResponse struct {
    Message    string `json:"message,omitempty"`
    StatusCode int    `json:"statusCode,omitempty"`
    ErrorMsg   string `json:"error,omitempty"`
    Details    string `json:"details,omitempty"`
}

Strategy

const (
    FixedDelayStrategy         Strategy = "fixed"
    JitterBackoffStrategy      Strategy = "jitter"
    ExponentialBackoffStrategy Strategy = "exponential"
)

Best Practices

1. Always Check for Errors

req, err := httpx.NewRequestBuilder(baseURL).
    WithMethodGET().
    WithPath("/endpoint").
    Build()

if err != nil {
    log.Printf("Request building failed: %v", err)
    return
}

2. Use Type-Safe Clients for JSON APIs

// Define your model
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// Create typed client
client := httpx.NewGenericClient[User](
    httpx.)

// Enjoy type safety
response, err := client.Get("/users/1")
// response.Data is User, not interface{}

3. Configure Retry Logic for Production

client := httpx.NewClientBuilder().
    WithMaxRetries(3).
    WithRetryStrategy(httpx.ExponentialBackoffStrategy).
    WithRetryBaseDelay(500 * time.Millisecond).
    WithRetryMaxDelay(10 * time.Second).
    WithTimeout(30 * time.Second).
    Build()

4. Reuse HTTP Clients

// Create once, reuse many times
retryClient := httpx.NewClientBuilder().
    WithMaxRetries(3).
    Build()

userClient := httpx.NewGenericClient[User](
    httpx.WithHTTPClient[User](retryClient),
)

postClient := httpx.NewGenericClient[Post](
    httpx.WithHTTPClient[Post](retryClient),
)

5. Use Context for Timeouts

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, err := httpx.NewRequestBuilder(baseURL).
    WithMethodGET().
    WithPath("/endpoint").
    Context(ctx).
    Build()

6. Validate Before Building

builder := httpx.NewRequestBuilder(baseURL).
    WithMethodGET().
    WithPath("/endpoint")

// Add potentially invalid inputs
builder.WithHeader(userProvidedKey, userProvidedValue)
builder.WithQueryParam(userProvidedParam, userProvidedValue)

// Check for errors before building
if builder.HasErrors() {
    for _, err := range builder.GetErrors() {
        log.Printf("Validation error: %v", err)
    }
    return
}

req, err := builder.Build()

7. Handle API Errors Properly

response, err := client.Get("/resource")
if err != nil {
    if apiErr, ok := err.(*httpx.ErrorResponse); ok {
        switch apiErr.StatusCode {
        case 404:
            log.Printf("Resource not found: %s", apiErr.Message)
        case 401:
            log.Printf("Authentication failed: %s", apiErr.Message)
        case 429:
            log.Printf("Rate limit exceeded: %s", apiErr.Message)
        default:
            log.Printf("API error %d: %s", apiErr.StatusCode, apiErr.Message)
        }
    } else {
        log.Printf("Network error: %v", err)
    }

    return
}

Thread Safety

All utilities in this package are safe for concurrent use:

client := httpx.NewGenericClient[User](
    httpx.)

// Safe to use from multiple goroutines
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        user, err := client.Get(fmt.Sprintf("/users/%d", id))
        if err != nil {
            log.Printf("Error fetching user %d: %v", id, err)
            return
        }
        log.Printf("Fetched user: %s", user.Data.Name)
    }(i)
}

wg.Wait()

Testing

The package has comprehensive test coverage (88%+):

go test ./... -v
go test ./... -cover

Contributing

Contributions are welcome! Please ensure:

  1. Build passes: go build ./...
  2. All tests pass: go test ./...
  3. Code is formatted: go fmt ./...
  4. Linters pass: golangci-lint run ./...
  5. Add tests for new features
  6. Update documentation

License

Apache License 2.0. See LICENSE for details.

Credits

Developed by the slashdevops team using Agentic Development. Inspired by popular HTTP client libraries and Go best practices.

About

A comprehensive Go package for building and executing HTTP requests with advanced features

Resources

License

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Languages