httpclient

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Dec 24, 2025 License: MIT Imports: 15 Imported by: 0

README

go-httpclient

Go Version License Go Report Card GoDoc

A production-ready HTTP client for Go with built-in resilience patterns including retry policies, circuit breakers, timeouts, and fallback mechanisms. Perfect for building reliable microservices and external API integrations.

Features

Resilience Patterns

  • 🔄 Retry Policy - Exponential backoff, jitter, max attempts/duration
  • Circuit Breaker - Prevent cascading failures with automatic recovery
  • ⏱️ Timeout - Request-level and global timeout controls
  • 🛡️ Fallback - Graceful degradation with custom fallback responses

🎯 Developer Experience

  • Simple, intuitive API with method chaining
  • JSON marshaling/unmarshaling built-in
  • Context support for cancellation and deadlines
  • Typed error handling with detailed context
  • Comprehensive test coverage (>95%)

🏗️ Production Ready

  • Built on failsafe-go - battle-tested resilience library
  • Clean Architecture principles
  • Zero external dependencies (except failsafe-go)
  • Fully documented with examples

Installation

go get github.com/ja7ad/httpclient

Examples

Basic
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/ja7ad/httpclient"
)

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

func main() {
	client := httpclient.NewDefaultClient("https://jsonplaceholder.typicode.com")

	var user User
	err := client.GetJSON(context.Background(), "/users/1", &user)
	if err != nil {
		log.Fatalf("Failed to get user: %v", err)
	}

	fmt.Printf("User ID: %d\n", user.ID)
	fmt.Printf("Name: %s\n", user.Name)
	fmt.Printf("Email: %s\n", user.Email)
	fmt.Printf("Username: %s\n", user.Username)
}
CRUD
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/ja7ad/httpclient"
)

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

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

func main() {
	client := httpclient.NewDefaultClient("https://jsonplaceholder.typicode.com")
	ctx := context.Background()

	fmt.Println("=== GET: Fetch a post ===")
	getExample(client, ctx)

	fmt.Println("\n=== POST: Create a new post ===")
	postExample(client, ctx)

	fmt.Println("\n=== PUT: Update a post ===")
	putExample(client, ctx)

	fmt.Println("\n=== DELETE: Delete a post ===")
	deleteExample(client, ctx)
}

func getExample(client *httpclient.Client, ctx context.Context) {
	var post Post
	err := client.GetJSON(ctx, "/posts/1", &post)
	if err != nil {
		log.Printf("GET failed: %v", err)
		return
	}

	fmt.Printf("Post: %+v\n", post)
}

func postExample(client *httpclient.Client, ctx context.Context) {
	newPost := CreatePostRequest{
		UserID: 1,
		Title:  "My New Post",
		Body:   "This is the content of my new post",
	}

	var createdPost Post
	err := client.PostJSON(ctx, "/posts", newPost, &createdPost)
	if err != nil {
		log.Printf("POST failed: %v", err)
		return
	}

	fmt.Printf("Created Post: %+v\n", createdPost)
}

func putExample(client *httpclient.Client, ctx context.Context) {
	updatedPost := CreatePostRequest{
		UserID: 1,
		Title:  "Updated Post Title",
		Body:   "This is the updated content",
	}

	var post Post
	err := client.PutJSON(ctx, "/posts/1", updatedPost, &post)
	if err != nil {
		log.Printf("PUT failed: %v", err)
		return
	}

	fmt.Printf("Updated Post: %+v\n", post)
}

func deleteExample(client *httpclient.Client, ctx context.Context) {
	resp, err := client.Delete(ctx, "/posts/1")
	if err != nil {
		log.Printf("DELETE failed: %v", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("Delete Status: %d\n", resp.StatusCode)
}
With Resilience
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/failsafe-go/failsafe-go"
	"github.com/failsafe-go/failsafe-go/circuitbreaker"
	"github.com/ja7ad/httpclient"
)

func main() {
	fmt.Println("=== Retry Example ===")
	retryExample()

	fmt.Println("\n=== Circuit Breaker Example ===")
	circuitBreakerExample()

	fmt.Println("\n=== Timeout Example ===")
	timeoutExample()

	fmt.Println("\n=== Combined Resilience Example ===")
	combinedExample()

	fmt.Println("\n=== Fallback Example ===")
	fallbackExample()
}

func retryExample() {
	cfg := &httpclient.ResilienceConfig{
		RetryPolicy: &httpclient.RetryPolicyConfig{
			Enabled:        true,
			MaxAttempts:    5,
			BackoffInitial: 100 * time.Millisecond,
			BackoffMax:     5 * time.Second,
			JitterFactor:   0.2,
			RetryableStatus: []int{
				http.StatusInternalServerError,
				http.StatusBadGateway,
				http.StatusServiceUnavailable,
				http.StatusGatewayTimeout,
			},
			OnRetry: func(event failsafe.ExecutionEvent[*http.Response]) {
				fmt.Printf("Retry attempt #%d\n", event.Attempts())
			},
		},
	}

	client := httpclient.NewClient(
		httpclient.WithBaseURL("https://httpstat.us"),
		httpclient.WithResilienceConfig(cfg),
	)

	resp, err := client.Get(context.Background(), "/200")
	if err != nil {
		log.Printf("Request failed: %v", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("Success! Status: %d\n", resp.StatusCode)
}

func circuitBreakerExample() {
	cfg := &httpclient.ResilienceConfig{
		CircuitBreaker: &httpclient.CircuitBreakerConfig{
			Enabled:          true,
			FailureThreshold: 3,
			SuccessThreshold: 2,
			Delay:            5 * time.Second,
			OnOpen: func(event circuitbreaker.StateChangedEvent) {
				fmt.Println("⚠️  Circuit breaker opened!")
			},
			OnClose: func(event circuitbreaker.StateChangedEvent) {
				fmt.Println("✅ Circuit breaker closed!")
			},
			OnHalfOpen: func(event circuitbreaker.StateChangedEvent) {
				fmt.Println("🔄 Circuit breaker half-open, testing...")
			},
		},
	}

	client := httpclient.NewClient(
		httpclient.WithBaseURL("https://httpstat.us"),
		httpclient.WithResilienceConfig(cfg),
	)

	for i := 1; i <= 5; i++ {
		fmt.Printf("\nRequest #%d\n", i)
		resp, err := client.Get(context.Background(), "/200")
		if err != nil {
			log.Printf("Request failed: %v", err)
			continue
		}
		resp.Body.Close()
		fmt.Printf("Success! Status: %d\n", resp.StatusCode)
	}
}

func timeoutExample() {
	cfg := &httpclient.ResilienceConfig{
		Timeout: &httpclient.TimeoutConfig{
			Enabled:  true,
			Duration: 2 * time.Second,
			OnTimeoutExceeded: func(event failsafe.ExecutionDoneEvent[*http.Response]) {
				fmt.Println("⏰ Request timed out!")
			},
		},
	}

	client := httpclient.NewClient(
		httpclient.WithBaseURL("https://httpstat.us"),
		httpclient.WithResilienceConfig(cfg),
	)

	resp, err := client.Get(context.Background(), "/200?sleep=1000")
	if err != nil {
		log.Printf("Request failed: %v", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("Success within timeout! Status: %d\n", resp.StatusCode)
}

func combinedExample() {
	cfg := &httpclient.ResilienceConfig{
		CircuitBreaker: &httpclient.CircuitBreakerConfig{
			Enabled:          true,
			FailureThreshold: 5,
			SuccessThreshold: 2,
			Delay:            10 * time.Second,
		},
		RetryPolicy: &httpclient.RetryPolicyConfig{
			Enabled:        true,
			MaxAttempts:    3,
			BackoffInitial: 100 * time.Millisecond,
			BackoffMax:     10 * time.Second,
			JitterFactor:   0.1,
			RetryableStatus: []int{
				http.StatusInternalServerError,
				http.StatusBadGateway,
				http.StatusServiceUnavailable,
			},
		},
		Timeout: &httpclient.TimeoutConfig{
			Enabled:  true,
			Duration: 30 * time.Second,
		},
	}

	client := httpclient.NewClient(
		httpclient.WithBaseURL("https://jsonplaceholder.typicode.com"),
		httpclient.WithResilienceConfig(cfg),
	)

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

	var user User
	err := client.GetJSON(context.Background(), "/users/1", &user)
	if err != nil {
		log.Printf("Request failed: %v", err)
		return
	}

	fmt.Printf("User: %+v\n", user)
}

func fallbackExample() {
	cfg := &httpclient.ResilienceConfig{
		Fallback: &httpclient.FallbackConfig{
			Enabled: true,
			FallbackFunc: func(exec failsafe.Execution[*http.Response]) (*http.Response, error) {
				fmt.Println("🛡️  Fallback triggered! Returning cached response...")
				
				cachedResponse := `{"id": 1, "name": "Cached User", "email": "cached@example.com"}`
				return &http.Response{
					StatusCode: http.StatusOK,
					Body:       http.NoBody,
					Header:     make(http.Header),
				}, nil
			},
			OnFallbackExecuted: func(event failsafe.ExecutionDoneEvent[*http.Response]) {
				fmt.Println("✅ Fallback executed successfully")
			},
		},
	}

	client := httpclient.NewClient(
		httpclient.WithBaseURL("https://invalid-domain-that-does-not-exist.com"),
		httpclient.WithResilienceConfig(cfg),
	)

	resp, err := client.Get(context.Background(), "/users/1")
	if err != nil {
		log.Printf("Request failed: %v", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("Response Status: %d\n", resp.StatusCode)
}
Custom Error Response
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/ja7ad/httpclient"
)

func main() {
	// Create client with automatic error parsing
	client := httpclient.NewDefaultClient("https://api.sumsub.com")
	client.SetHeader("Authorization", "Bearer your-token")

	var result map[string]interface{}
	err := client.GetJSON(context.Background(), "/resources/applicants/12313213", &result)
	
	if err != nil {
		var httpErr *httpclient.Error
		if errors.As(err, &httpErr) {
			fmt.Printf("Error: %s\n", httpErr.Message)
			fmt.Printf("Status Code: %d\n", httpErr.StatusCode)
			fmt.Printf("Error Code: %s\n", httpErr.ErrorCode)
			fmt.Printf("Correlation ID: %s\n", httpErr.CorrelationID)
			
			// Access specific details
			if desc, ok := httpErr.GetDetail("description"); ok {
				fmt.Printf("Description: %v\n", desc)
			}
			
			// Check all details
			for key, value := range httpErr.Details {
				fmt.Printf("%s: %v\n", key, value)
			}
		}
	}
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewResilientClient

func NewResilientClient(cfg *ResilienceConfig) *http.Client

NewResilientClient creates an HTTP client with resilience policies This is the recommended way to create a resilient HTTP client

func NewResilientTransport

func NewResilientTransport(baseTransport http.RoundTripper, cfg *ResilienceConfig) http.RoundTripper

NewResilientTransport wraps an existing transport with resilience policies Use this if you need to customize the base transport

Types

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	Enabled               bool
	FailureThreshold      uint            // Count-based: Opens after N consecutive failures
	FailureThresholdRatio *ThresholdRatio // Ratio-based: Opens when X of Y executions fail
	SuccessThreshold      uint            // Count-based: Closes after N consecutive successes in half-open
	SuccessThresholdRatio *ThresholdRatio // Ratio-based: Closes when X of Y executions succeed in half-open
	Delay                 time.Duration   // Time to wait in open state before half-opening
	OnStateChanged        func(event circuitbreaker.StateChangedEvent)
	OnOpen                func(event circuitbreaker.StateChangedEvent)
	OnHalfOpen            func(event circuitbreaker.StateChangedEvent)
	OnClose               func(event circuitbreaker.StateChangedEvent)
}

CircuitBreakerConfig holds circuit breaker configuration

type Client

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

func NewClient

func NewClient(opts ...ClientOption) *Client

func NewDefaultClient

func NewDefaultClient(baseURL string) *Client

func (*Client) Delete

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

func (*Client) DeleteJSON

func (c *Client) DeleteJSON(ctx context.Context, path string, result interface{}) error

func (*Client) Do

func (c *Client) Do(ctx context.Context, method, path string, body interface{}) (*http.Response, error)

func (*Client) DoWithResponse

func (c *Client) DoWithResponse(ctx context.Context, method, path string, body, result interface{}) error

func (*Client) Get

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

func (*Client) GetErrorParser added in v1.1.0

func (c *Client) GetErrorParser() *ErrorParserChain

func (*Client) GetHTTPClient

func (c *Client) GetHTTPClient() *http.Client

func (*Client) GetJSON

func (c *Client) GetJSON(ctx context.Context, path string, result interface{}) error

func (*Client) Patch

func (c *Client) Patch(ctx context.Context, path string, body interface{}) (*http.Response, error)

func (*Client) PatchJSON

func (c *Client) PatchJSON(ctx context.Context, path string, body, result interface{}) error

func (*Client) Post

func (c *Client) Post(ctx context.Context, path string, body interface{}) (*http.Response, error)

func (*Client) PostJSON

func (c *Client) PostJSON(ctx context.Context, path string, body, result interface{}) error

func (*Client) Put

func (c *Client) Put(ctx context.Context, path string, body interface{}) (*http.Response, error)

func (*Client) PutJSON

func (c *Client) PutJSON(ctx context.Context, path string, body, result interface{}) error

func (*Client) RemoveHeader

func (c *Client) RemoveHeader(key string)

func (*Client) SetBaseURL

func (c *Client) SetBaseURL(baseURL string)

func (*Client) SetErrorParser added in v1.1.0

func (c *Client) SetErrorParser(parser *ErrorParserChain)

func (*Client) SetHTTPClient

func (c *Client) SetHTTPClient(httpClient *http.Client)

func (*Client) SetHeader

func (c *Client) SetHeader(key, value string)

func (*Client) SetHeaders

func (c *Client) SetHeaders(headers map[string]string)

type ClientOption

type ClientOption func(*Client)

func WithBaseURL

func WithBaseURL(baseURL string) ClientOption

func WithErrorParser added in v1.1.0

func WithErrorParser(parser ErrorResponseParser) ClientOption

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) ClientOption

func WithHeader

func WithHeader(key, value string) ClientOption

func WithHeaders

func WithHeaders(headers map[string]string) ClientOption

func WithResilienceConfig

func WithResilienceConfig(cfg *ResilienceConfig) ClientOption

func WithTimeout

func WithTimeout(timeout time.Duration) ClientOption

type Error

type Error struct {
	Type          ErrorType              `json:"type"`
	Message       string                 `json:"message"`
	StatusCode    int                    `json:"status_code,omitempty"`
	URL           string                 `json:"url"`
	Method        string                 `json:"method"`
	RequestID     string                 `json:"request_id,omitempty"`
	CorrelationID string                 `json:"correlation_id,omitempty"`
	ErrorCode     string                 `json:"error_code,omitempty"`
	Details       map[string]interface{} `json:"details,omitempty"`
	Err           error                  `json:"-"`
	Timestamp     time.Time              `json:"timestamp"`
	ResponseBody  []byte                 `json:"-"`
}

Error represents an HTTP client error with detailed context

func (*Error) Error

func (e *Error) Error() string

func (*Error) GetDetail added in v1.1.0

func (e *Error) GetDetail(key string) (interface{}, bool)

GetDetail retrieves a specific detail from the error response

func (*Error) HasDetail added in v1.1.0

func (e *Error) HasDetail(key string) bool

HasDetail checks if a specific detail key exists

func (*Error) Unwrap

func (e *Error) Unwrap() error

type ErrorParserChain added in v1.1.0

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

ErrorParserChain tries multiple parsers in order

func NewErrorParserChain added in v1.1.0

func NewErrorParserChain() *ErrorParserChain

func (*ErrorParserChain) AddParser added in v1.1.0

func (c *ErrorParserChain) AddParser(parser ErrorResponseParser)

func (*ErrorParserChain) Parse added in v1.1.0

func (c *ErrorParserChain) Parse(body []byte) (*ParsedErrorResponse, error)

type ErrorResponseParser added in v1.1.0

type ErrorResponseParser interface {
	Parse(body []byte) (*ParsedErrorResponse, error)
	CanParse(body []byte) bool
}

ErrorResponseParser defines an interface for parsing custom error responses

type ErrorType

type ErrorType string
const (
	ErrorTypeTimeout        ErrorType = "timeout"
	ErrorTypeNetwork        ErrorType = "network"
	ErrorTypeHTTP           ErrorType = "http"
	ErrorTypeRetryExhausted ErrorType = "retry_exhausted"
	ErrorTypeValidation     ErrorType = "validation"
	ErrorTypeUnknown        ErrorType = "unknown"
)

type FallbackConfig

type FallbackConfig struct {
	Enabled            bool
	FallbackFunc       func(exec failsafe.Execution[*http.Response]) (*http.Response, error)
	OnFallbackExecuted func(event failsafe.ExecutionDoneEvent[*http.Response])
}

FallbackConfig holds fallback configuration

type GenericErrorParser added in v1.1.0

type GenericErrorParser struct{}

GenericErrorParser parses common error response formats

func (*GenericErrorParser) CanParse added in v1.1.0

func (p *GenericErrorParser) CanParse(body []byte) bool

func (*GenericErrorParser) Parse added in v1.1.0

func (p *GenericErrorParser) Parse(body []byte) (*ParsedErrorResponse, error)

type ParsedErrorResponse added in v1.1.0

type ParsedErrorResponse struct {
	Message       string
	ErrorCode     string
	CorrelationID string
	Details       map[string]interface{}
}

ParsedErrorResponse represents a parsed error response from an external API

type RFC7807ErrorParser added in v1.1.0

type RFC7807ErrorParser struct {
	GenericErrorParser
}

RFC7807ErrorParser parses RFC 7807 Problem Details for HTTP APIs

func (*RFC7807ErrorParser) CanParse added in v1.1.0

func (p *RFC7807ErrorParser) CanParse(body []byte) bool

func (*RFC7807ErrorParser) Parse added in v1.1.0

func (p *RFC7807ErrorParser) Parse(body []byte) (*ParsedErrorResponse, error)

type ResilienceBuilder

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

ResilienceBuilder builds resilience configurations using fluent API

func NewResilienceBuilder

func NewResilienceBuilder() *ResilienceBuilder

NewResilienceBuilder creates a new resilience configuration builder

func (*ResilienceBuilder) Build

func (rb *ResilienceBuilder) Build() *ResilienceConfig

Build constructs the final resilience configuration

func (*ResilienceBuilder) WithCircuitBreaker

func (rb *ResilienceBuilder) WithCircuitBreaker(cfg *CircuitBreakerConfig) *ResilienceBuilder

WithCircuitBreaker configures circuit breaker settings

func (*ResilienceBuilder) WithFallback

func (rb *ResilienceBuilder) WithFallback(cfg *FallbackConfig) *ResilienceBuilder

WithFallback configures fallback settings

func (*ResilienceBuilder) WithRetryPolicy

func (rb *ResilienceBuilder) WithRetryPolicy(cfg *RetryPolicyConfig) *ResilienceBuilder

WithRetryPolicy configures retry policy settings

func (*ResilienceBuilder) WithTimeout

func (rb *ResilienceBuilder) WithTimeout(cfg *TimeoutConfig) *ResilienceBuilder

WithTimeout configures timeout settings

type ResilienceConfig

type ResilienceConfig struct {
	CircuitBreaker *CircuitBreakerConfig
	RetryPolicy    *RetryPolicyConfig
	Timeout        *TimeoutConfig
	Fallback       *FallbackConfig
}

ResilienceConfig holds all resilience configurations

func DefaultResilienceConfig

func DefaultResilienceConfig() *ResilienceConfig

DefaultResilienceConfig returns a production-ready default configuration

type RetryPolicyConfig

type RetryPolicyConfig struct {
	Enabled           bool
	MaxAttempts       int           // Maximum number of execution attempts (initial + retries)
	MaxRetries        int           // Maximum number of retries (MaxAttempts - 1)
	MaxDuration       time.Duration // Maximum total duration for all attempts
	Delay             time.Duration // Fixed delay between retries
	BackoffInitial    time.Duration // Initial delay for exponential backoff
	BackoffMax        time.Duration // Maximum delay for exponential backoff
	RandomDelayMin    time.Duration // Minimum random delay
	RandomDelayMax    time.Duration // Maximum random delay
	JitterFactor      float64       // Jitter factor (0-1) to add randomness to delays
	Jitter            time.Duration // Time-based jitter to add to delays
	RetryableStatus   []int         // HTTP status codes that should trigger a retry
	AbortOnStatus     []int         // HTTP status codes that should abort retries immediately
	OnRetry           func(event failsafe.ExecutionEvent[*http.Response])
	OnRetriesExceeded func(event failsafe.ExecutionEvent[*http.Response])
	OnAbort           func(event failsafe.ExecutionEvent[*http.Response])
}

RetryPolicyConfig holds retry policy configuration

type StripeErrorParser added in v1.1.0

type StripeErrorParser struct {
	GenericErrorParser
}

StripeErrorParser parses Stripe-specific error responses

func (*StripeErrorParser) CanParse added in v1.1.0

func (p *StripeErrorParser) CanParse(body []byte) bool

func (*StripeErrorParser) Parse added in v1.1.0

func (p *StripeErrorParser) Parse(body []byte) (*ParsedErrorResponse, error)

type SumsubErrorParser added in v1.1.0

type SumsubErrorParser struct {
	GenericErrorParser
}

SumsubErrorParser parses Sumsub-specific error responses

func (*SumsubErrorParser) CanParse added in v1.1.0

func (p *SumsubErrorParser) CanParse(body []byte) bool

func (*SumsubErrorParser) Parse added in v1.1.0

func (p *SumsubErrorParser) Parse(body []byte) (*ParsedErrorResponse, error)

type ThresholdRatio

type ThresholdRatio struct {
	Failures uint // Number of failures
	Total    uint // Total executions
}

ThresholdRatio represents a ratio-based threshold (e.g., 3 failures out of 5 total)

type TimeoutConfig

type TimeoutConfig struct {
	Enabled           bool
	Duration          time.Duration // Maximum time allowed for execution
	OnTimeoutExceeded func(event failsafe.ExecutionDoneEvent[*http.Response])
}

TimeoutConfig holds timeout configuration

Jump to

Keyboard shortcuts

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