Skip to content

JnaneshD/httpc

Repository files navigation

httpc

Go Reference Go Report Card License: MIT

A fluent, batteries-included HTTP client wrapper for Go that makes common patterns trivial while staying close to Go's standard library philosophy.

Features

  • 🔗 Fluent & Chainable - Easy to read and write API
  • Sensible Defaults - Works great out of the box
  • 🎯 Gradual Complexity - Simple things simple, complex things possible
  • 🛡️ Production Ready - Retries, timeouts, circuit breakers included
  • 🧪 Testable - Easy mocking and request recording
  • 📝 Well Documented - Comprehensive examples and documentation

Installation

go get github.com/jnaneshd/httpc

Quick Start

Simple GET Request

resp, err := httpc.New().
    Get("https://api.example.com/users").
    Do()
if err != nil {
    log.Fatal(err)
}
defer resp.Close()

fmt.Println(resp.Status())

POST with JSON

user := User{Name: "John", Email: "john@example.com"}
resp, err := httpc.New().
    Post("https://api.example.com/users").
    JSON(user).
    Do()

Get Raw Response (String or Bytes)

// Get response as string - perfect for printing JSON
jsonStr, err := httpc.New().
    Get("https://api.example.com/users").
    String()

fmt.Println(jsonStr)  // {"id": 1, "name": "John", ...}

// Or as bytes
data, err := httpc.New().
    Get("https://api.example.com/users").
    Bytes()

Decode JSON Response

var users []User
err := httpc.New().
    Get("https://api.example.com/users").
    DecodeJSON(&users)

With Headers and Query Parameters

resp, err := httpc.New().
    Get("https://api.example.com/users").
    Header("Authorization", "Bearer token").
    Query("page", "1").
    Query("limit", "10").
    Do()

Configuration

Base URL and Default Headers

client := httpc.New().
    WithBaseURL("https://api.example.com").
    WithHeader("User-Agent", "MyApp/1.0").
    WithHeader("Accept", "application/json")

// Now all requests use the base URL
resp, err := client.Get("/users").Do()  // GET https://api.example.com/users

Timeout

client := httpc.New().
    WithTimeout(30 * time.Second)

// Or per-request timeout
resp, err := client.Get("/slow-endpoint").
    Timeout(60 * time.Second).
    Do()

Resilience Features

Automatic Retries

client := httpc.New().
    WithRetry(3, time.Second)  // 3 attempts, 1s initial delay

// Or with custom configuration
client := httpc.New().
    WithRetryConfig(&httpc.RetryConfig{
        MaxAttempts:  5,
        InitialDelay: 500 * time.Millisecond,
        MaxDelay:     30 * time.Second,
        Multiplier:   2.0,  // Exponential backoff
        Jitter:       true, // Randomize delays
    })

Circuit Breaker

Prevent cascading failures by stopping requests to failing services:

client := httpc.New().
    WithCircuitBreaker(5, time.Minute)  // Open after 5 failures, try again after 1 min

// Or with custom configuration
client := httpc.New().
    WithCircuitBreakerConfig(&httpc.CircuitBreakerConfig{
        Name:        "my-service",
        MaxFailures: 5,
        Timeout:     60 * time.Second,
        OnStateChange: func(name string, from, to gobreaker.State) {
            log.Printf("Circuit breaker %s: %s -> %s", name, from, to)
        },
    })

Rate Limiting

client := httpc.New().
    WithRateLimiter(10)  // 10 requests per second

// Or with burst capacity
client := httpc.New().
    WithRateLimiterConfig(10, 20)  // 10 req/s with burst of 20

Middleware

Add cross-cutting concerns like authentication, logging, or metrics:

import "net/http"

// Authentication middleware
authMiddleware := func(req *http.Request, next httpc.HTTPHandler) (*httpc.Response, error) {
    req.Header.Set("Authorization", "Bearer " + getToken())
    return next(req)
}

// Logging middleware
loggingMiddleware := func(req *http.Request, next httpc.HTTPHandler) (*httpc.Response, error) {
    start := time.Now()
    resp, err := next(req)
    log.Printf("%s %s took %v", req.Method, req.URL, time.Since(start))
    return resp, err
}

client := httpc.New().
    WithMiddleware(authMiddleware).
    WithMiddleware(loggingMiddleware)

Built-in Middlewares

// Bearer token
client.WithMiddleware(httpc.BearerTokenMiddleware("your-token"))

// Dynamic token (refreshed on each request)
client.WithMiddleware(httpc.DynamicBearerTokenMiddleware(func() (string, error) {
    return refreshToken()
}))

// User-Agent
client.WithMiddleware(httpc.UserAgentMiddleware("MyApp/1.0"))

// Default headers
client.WithMiddleware(httpc.HeadersMiddleware(map[string]string{
    "X-API-Key": "secret",
}))

Request Building

Body Types

// JSON
client.Post("/users").JSON(user)

// Form data
client.Post("/login").Form(map[string]string{
    "username": "john",
    "password": "secret",
})

// Raw bytes
client.Post("/upload").BodyBytes(data)

// String
client.Post("/text").BodyString("Hello, World!")

// Reader
client.Post("/stream").Body(reader)

Authentication

// Bearer token
client.Get("/protected").Bearer("your-token")

// Basic auth
client.Get("/protected").BasicAuth("username", "password")

Context

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

resp, err := client.Get("/users").Context(ctx).Do()

Expected Status

// Returns error if status doesn't match
resp, err := client.Post("/users").
    JSON(user).
    ExpectStatus(201).
    Do()

Response Handling

Quick Methods (One-liners)

These methods execute the request and return the result directly:

// Get raw JSON string - no decoding needed
jsonStr, err := client.Get("/users").String()
fmt.Println(jsonStr)  // prints raw JSON

// Get raw bytes
data, err := client.Get("/file").Bytes()

// Decode JSON directly
var users []User
err := client.Get("/users").DecodeJSON(&users)

Full Response Access

For more control, use .Do() to get the full response:

resp, err := client.Get("/users").Do()
if err != nil {
    log.Fatal(err)
}
defer resp.Close()

// Status
fmt.Println(resp.StatusCode())    // 200
fmt.Println(resp.Status())        // "200 OK"
fmt.Println(resp.IsSuccess())     // true (2xx)
fmt.Println(resp.IsError())       // false (4xx or 5xx)
fmt.Println(resp.IsClientError()) // false (4xx)
fmt.Println(resp.IsServerError()) // false (5xx)

// Headers
fmt.Println(resp.Header("Content-Type"))
fmt.Println(resp.ContentType())
fmt.Println(resp.ContentLength())

// Body (multiple ways)
body, _ := resp.String()    // As string (for printing/logging)
bytes, _ := resp.Bytes()    // As []byte (for binary data)
var data MyStruct
resp.JSON(&data)            // Decode JSON into struct

// Cookies
cookies := resp.Cookies()

Testing

Mock Server

import "github.com/jnaneshd/httpc/mock"

func TestAPI(t *testing.T) {
    server := mock.NewServer()
    defer server.Close()

    // Set up mock response
    server.OnGet("/users/1").
        Status(200).
        JSON(User{ID: 1, Name: "John"}).
        Reply()

    // Use mock server URL
    client := httpc.New().WithBaseURL(server.URL())

    var user User
    err := client.Get("/users/1").DecodeJSON(&user)
    if err != nil {
        t.Fatal(err)
    }

    // Verify request was made
    if server.RequestCount() != 1 {
        t.Errorf("expected 1 request, got %d", server.RequestCount())
    }
}

Custom Handler

server.OnPost("/users").
    Handler(func(w http.ResponseWriter, r *http.Request) {
        // Verify request
        if r.Header.Get("Authorization") == "" {
            w.WriteHeader(401)
            return
        }
        w.WriteHeader(201)
        w.Write([]byte(`{"id": 1}`))
    }).
    Reply()

Request Recording

// Access recorded requests
requests := server.Requests()
lastReq := server.LastRequest()

fmt.Println(lastReq.Method)  // "POST"
fmt.Println(lastReq.Path)    // "/users"
fmt.Println(lastReq.Headers) // http.Header

Full Example

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/jnaneshd/httpc"
)

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

func main() {
    // Create a production-ready client
    client := httpc.New().
        WithBaseURL("https://api.example.com").
        WithHeader("User-Agent", "MyApp/1.0").
        WithTimeout(30 * time.Second).
        WithRetry(3, time.Second).
        WithCircuitBreaker(5, time.Minute).
        WithRateLimiter(100).
        WithMiddleware(httpc.BearerTokenMiddleware(getToken()))

    // Create a user
    newUser := User{Name: "John", Email: "john@example.com"}
    var createdUser User

    err := client.
        Post("/users").
        JSON(newUser).
        ExpectStatus(201).
        DecodeJSON(&createdUser)

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

    fmt.Printf("Created user: %+v\n", createdUser)

    // Get all users
    var users []User
    err = client.
        Get("/users").
        Query("limit", "10").
        DecodeJSON(&users)

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

    fmt.Printf("Found %d users\n", len(users))
}

func getToken() string {
    return "your-api-token"
}

Error Handling

resp, err := client.Get("/users").Do()
if err != nil {
    var httpErr *httpc.Error
    if errors.As(err, &httpErr) {
        fmt.Println("Operation:", httpErr.Op)
        fmt.Println("URL:", httpErr.URL)
        fmt.Println("Status:", httpErr.StatusCode)
        fmt.Println("Retryable:", httpErr.IsRetryable())
    }
}

Sentinel Errors

if errors.Is(err, httpc.ErrCircuitOpen) {
    // Circuit breaker is open
}
if errors.Is(err, httpc.ErrRateLimited) {
    // Rate limit exceeded
}
if errors.Is(err, httpc.ErrRetryExhausted) {
    // All retry attempts failed
}

Comparison

Feature httpc net/http resty
Fluent API
JSON helpers
Retries
Circuit breaker
Rate limiting
Mock server
Middleware
Lightweight

Requirements

  • Go 1.21 or later

Dependencies

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Author

jnaneshd

About

A fluent, batteries-included HTTP client wrapper for Go

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages