rest

package
v2.7.2 Latest Latest
Warning

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

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

README

REST Client

Go Reference

Resilient HTTP client with automatic retries, OpenTelemetry instrumentation, and middleware support built on Resty.

Overview

The rest package provides a production-ready HTTP client with built-in resilience patterns, observability, and extensibility through middleware. Built on top of go-resty, it adds OpenTelemetry tracing, metrics, and customizable request/response processing.

Features

  • Automatic Retries: Configurable retry logic with exponential backoff
  • OpenTelemetry Integration: Distributed tracing and metrics
  • Middleware System: Extensible request/response processing
  • Timeout Management: Request-level timeout configuration
  • Thread-Safe: Concurrent-safe middleware management
  • Flexible API: Support for all HTTP methods

Installation

go get github.com/jasoet/pkg/v2/rest

Quick Start

Basic Usage
package main

import (
    "context"
    "github.com/jasoet/pkg/v2/rest"
)

func main() {
    // Create client with default config
    client := rest.NewClient()

    // Make request
    ctx := context.Background()
    response, err := client.MakeRequestWithTrace(
        ctx,
        "GET",
        "https://api.example.com/users",
        "",
        nil,
    )

    if err != nil {
        panic(err)
    }

    fmt.Println(response.String())
}
With Custom Configuration
import (
    "time"
    "github.com/jasoet/pkg/v2/rest"
)

config := rest.Config{
    RetryCount:       3,
    RetryWaitTime:    1 * time.Second,
    RetryMaxWaitTime: 5 * time.Second,
    Timeout:          30 * time.Second,
}

client := rest.NewClient(
    rest.WithRestConfig(config),
)
With OpenTelemetry
import (
    "github.com/jasoet/pkg/v2/rest"
    "github.com/jasoet/pkg/v2/otel"
)

// Setup OTel
otelConfig := otel.NewConfig("my-service").
    WithTracerProvider(tracerProvider).
    WithMeterProvider(meterProvider)

// Create client with OTel
client := rest.NewClient(
    rest.WithOTelConfig(otelConfig),
)

// All requests are automatically traced
response, err := client.MakeRequestWithTrace(ctx, "GET", url, "", nil)

Configuration

Config Struct
type Config struct {
    RetryCount       int           // Number of retry attempts
    RetryWaitTime    time.Duration // Initial retry wait time
    RetryMaxWaitTime time.Duration // Maximum retry wait time
    Timeout          time.Duration // Request timeout

    // Optional: Enable OpenTelemetry (nil = disabled)
    OTelConfig       *otel.Config
}
Default Configuration
DefaultRestConfig() returns:
- RetryCount:       1
- RetryWaitTime:    20 seconds
- RetryMaxWaitTime: 30 seconds
- Timeout:          50 seconds

Client API

Client Options
// Set configuration
WithRestConfig(config Config)

// Add single middleware
WithMiddleware(middleware Middleware)

// Set multiple middlewares
WithMiddlewares(middlewares ...Middleware)

// Enable OpenTelemetry
WithOTelConfig(cfg *otel.Config)
Methods
// Make HTTP request with tracing
MakeRequestWithTrace(
    ctx context.Context,
    method string,
    url string,
    body string,
    headers map[string]string,
) (*resty.Response, error)

// Get underlying Resty client
GetRestClient() *resty.Client

// Get current configuration
GetRestConfig() *Config

// Middleware management
AddMiddleware(middleware Middleware)
SetMiddlewares(middlewares ...Middleware)
GetMiddlewares() []Middleware

Middleware System

Built-in Middleware
LoggingMiddleware

Logs request and response details:

client := rest.NewClient(
    rest.WithMiddleware(rest.NewLoggingMiddleware()),
)

// Logs:
// - Method, URL
// - Status code
// - Duration
// - Errors
NoOpMiddleware

Placeholder middleware for testing:

client := rest.NewClient(
    rest.WithMiddleware(rest.NewNoOpMiddleware()),
)
OpenTelemetry Middlewares

Automatically added when OTelConfig is provided:

  1. OTelTracingMiddleware - Distributed tracing
  2. OTelMetricsMiddleware - HTTP client metrics
  3. OTelLoggingMiddleware - Structured logging
Custom Middleware

Implement the Middleware interface:

type Middleware interface {
    BeforeRequest(
        ctx context.Context,
        method string,
        url string,
        body string,
        headers map[string]string,
    ) context.Context

    AfterRequest(ctx context.Context, info RequestInfo)
}

Example:

type AuthMiddleware struct {
    apiKey string
}

func (m *AuthMiddleware) BeforeRequest(
    ctx context.Context,
    method string,
    url string,
    body string,
    headers map[string]string,
) context.Context {
    headers["Authorization"] = "Bearer " + m.apiKey
    return ctx
}

func (m *AuthMiddleware) AfterRequest(
    ctx context.Context,
    info RequestInfo,
) {
    // Process response
}

// Usage
client := rest.NewClient(
    rest.WithMiddleware(&AuthMiddleware{apiKey: "secret"}),
)

OpenTelemetry Integration

Automatic Tracing

When OTelConfig is provided, all requests are traced:

otelConfig := otel.NewConfig("my-client").
    WithTracerProvider(tracerProvider)

client := rest.NewClient(
    rest.WithOTelConfig(otelConfig),
)

// Creates span for each request
response, _ := client.MakeRequestWithTrace(ctx, "GET", url, "", nil)
Span Attributes

Each HTTP request span includes:

Span Attributes:
  http.method: "GET" | "POST" | "PUT" | "DELETE" | ...
  http.url: "https://api.example.com/users"
  http.status_code: 200
  http.duration_ms: 150
  pkg.rest.client.name: "my-client"
  pkg.rest.retry.max_count: 3
  pkg.rest.timeout_ms: 30000
Metrics Collection

Automatic HTTP client metrics:

Metrics:
  http.client.request.duration: Histogram of request durations
  http.client.request.count: Counter of total requests
  http.client.request.active: Gauge of active requests

Attributes:
  http.method: "GET"
  http.status_code: 200
  service.name: "my-client"

Advanced Usage

All HTTP Methods
// GET
response, _ := client.MakeRequestWithTrace(ctx, "GET", url, "", headers)

// POST
response, _ := client.MakeRequestWithTrace(ctx, "POST", url, `{"key":"value"}`, headers)

// PUT
response, _ := client.MakeRequestWithTrace(ctx, "PUT", url, body, headers)

// DELETE
response, _ := client.MakeRequestWithTrace(ctx, "DELETE", url, "", headers)

// PATCH
response, _ := client.MakeRequestWithTrace(ctx, "PATCH", url, body, headers)

// HEAD
response, _ := client.MakeRequestWithTrace(ctx, "HEAD", url, "", headers)

// OPTIONS
response, _ := client.MakeRequestWithTrace(ctx, "OPTIONS", url, "", headers)
Custom Headers
headers := map[string]string{
    "Authorization": "Bearer token",
    "Content-Type":  "application/json",
    "X-API-Key":     "secret",
}

response, _ := client.MakeRequestWithTrace(ctx, "GET", url, "", headers)
Request Body
body := `{
    "name": "John Doe",
    "email": "john@example.com"
}`

response, _ := client.MakeRequestWithTrace(ctx, "POST", url, body, headers)
Configuration from YAML
import (
    "github.com/jasoet/pkg/v2/config"
    "github.com/jasoet/pkg/v2/rest"
)

type AppConfig struct {
    REST rest.Config `yaml:"rest"`
}

yamlConfig := `
rest:
  retryCount: 3
  retryWaitTime: 1s
  retryMaxWaitTime: 5s
  timeout: 30s
`

cfg, _ := config.LoadString[AppConfig](yamlConfig)
client := rest.NewClient(rest.WithRestConfig(cfg.REST))
Access Underlying Resty Client

For advanced Resty features:

client := rest.NewClient()

// Get Resty client
restyClient := client.GetRestClient()

// Use Resty directly
restyClient.R().
    SetHeader("X-Custom", "value").
    SetQueryParam("page", "1").
    Get("https://api.example.com/users")

Error Handling

response, err := client.MakeRequestWithTrace(ctx, "GET", url, "", nil)

if err != nil {
    // Network error, timeout, or other client error
    log.Printf("Request failed: %v", err)
    return
}

// Check HTTP status
if response.StatusCode() != 200 {
    log.Printf("HTTP error: %d - %s", response.StatusCode(), response.String())
    return
}

// Process response
fmt.Println(response.String())

Best Practices

1. Use Context for Cancellation
// ✅ Good: Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

response, err := client.MakeRequestWithTrace(ctx, "GET", url, "", nil)
2. Configure Retries Appropriately
// ✅ Good: Reasonable retry config
config := rest.Config{
    RetryCount:       3,              // Retry up to 3 times
    RetryWaitTime:    1 * time.Second, // Start with 1s
    RetryMaxWaitTime: 10 * time.Second, // Cap at 10s
    Timeout:          30 * time.Second,
}
3. Always Enable OTel in Production
// ✅ Good: Observability enabled
client := rest.NewClient(
    rest.WithOTelConfig(otelConfig),
)

// ❌ Bad: No observability
client := rest.NewClient()
4. Reuse Client Instances
// ✅ Good: Singleton client
var httpClient = rest.NewClient(/* config */)

func fetchUser(id string) {
    httpClient.MakeRequestWithTrace(/* ... */)
}

// ❌ Bad: New client per request
func fetchUser(id string) {
    client := rest.NewClient() // Creates new connection pool
    client.MakeRequestWithTrace(/* ... */)
}
5. Use Middleware for Cross-Cutting Concerns
// ✅ Good: Centralized auth
type AuthMiddleware struct { /* ... */ }

client := rest.NewClient(
    rest.WithMiddleware(&AuthMiddleware{}),
    rest.WithMiddleware(&RateLimitMiddleware{}),
)

// All requests get auth + rate limiting

Testing

The package includes comprehensive tests with 93% coverage:

# Run tests
go test ./rest -v

# With coverage
go test ./rest -cover
Test Utilities
import (
    "github.com/jasoet/pkg/v2/rest"
    "net/http/httptest"
)

func TestMyCode(t *testing.T) {
    // Mock server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte(`{"status":"ok"}`))
    }))
    defer server.Close()

    // Use no-op middleware for testing
    client := rest.NewClient(
        rest.WithMiddleware(rest.NewNoOpMiddleware()),
    )

    response, err := client.MakeRequestWithTrace(
        context.Background(),
        "GET",
        server.URL,
        "",
        nil,
    )

    assert.NoError(t, err)
    assert.Equal(t, 200, response.StatusCode())
}

Troubleshooting

Timeout Errors

Problem: Requests timing out

Solutions:

// 1. Increase timeout
config := rest.Config{
    Timeout: 60 * time.Second, // Longer timeout
    // ...
}

// 2. Use context timeout
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
Retry Not Working

Problem: Client not retrying failed requests

Solutions:

// 1. Check retry configuration
config := rest.Config{
    RetryCount:       3,              // Must be > 0
    RetryWaitTime:    1 * time.Second,
    RetryMaxWaitTime: 5 * time.Second,
}

// 2. Verify error is retryable
// Resty retries on network errors and 5xx status codes
// Does NOT retry on 4xx client errors
OTel Not Tracing

Problem: No spans appearing

Solutions:

// 1. Verify OTel config is provided
client := rest.NewClient(
    rest.WithOTelConfig(otelConfig), // Must be set
)

// 2. Check tracer provider
if otelConfig.IsTracingEnabled() {
    // Tracing is enabled
}

// 3. Ensure context propagation
ctx, span := tracer.Start(ctx, "parent-span")
defer span.End()

client.MakeRequestWithTrace(ctx, /* ... */) // Propagates context

Performance

  • Connection Pooling: Reuses HTTP connections via Resty
  • Low Overhead: Minimal middleware overhead (~microseconds)
  • Efficient Retries: Exponential backoff prevents thundering herd

Benchmark (typical request):

BenchmarkRequest-8         1000    ~1ms/op (including network)
BenchmarkMiddleware-8     10000    ~5µs/op (middleware overhead)

Examples

See examples/ directory for:

  • Basic HTTP requests
  • OpenTelemetry integration
  • Custom middleware
  • Error handling
  • Retry configuration
  • Authentication patterns
  • otel - OpenTelemetry configuration
  • config - Configuration management
  • server - HTTP server

License

MIT License - see LICENSE for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsClientError

func IsClientError(response *resty.Response) bool

func IsNotFound

func IsNotFound(response *resty.Response) bool

func IsServerError

func IsServerError(response *resty.Response) bool

func IsUnauthorized

func IsUnauthorized(response *resty.Response) bool

Types

type Client

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

func NewClient

func NewClient(options ...ClientOption) *Client

func (*Client) AddMiddleware

func (c *Client) AddMiddleware(middleware Middleware)

func (*Client) GetMiddlewares

func (c *Client) GetMiddlewares() []Middleware

func (*Client) GetRestClient

func (c *Client) GetRestClient() *resty.Client

func (*Client) GetRestConfig

func (c *Client) GetRestConfig() *Config

func (*Client) HandleResponse

func (c *Client) HandleResponse(response *resty.Response) error

func (*Client) MakeRequest

func (c *Client) MakeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) (*resty.Response, error)

func (*Client) MakeRequestWithTrace

func (c *Client) MakeRequestWithTrace(ctx context.Context, method string, url string, body string, headers map[string]string) (*resty.Response, error)

func (*Client) SetMiddlewares

func (c *Client) SetMiddlewares(middlewares ...Middleware)

type ClientOption

type ClientOption func(*Client)

func WithMiddleware

func WithMiddleware(middleware Middleware) ClientOption

func WithMiddlewares

func WithMiddlewares(middlewares ...Middleware) ClientOption

func WithOTelConfig

func WithOTelConfig(cfg *otel.Config) ClientOption

WithOTelConfig sets the OpenTelemetry configuration for the REST client When set, adds OTel tracing, metrics, and logging middleware automatically

func WithRestConfig

func WithRestConfig(restConfig Config) ClientOption

type Config

type Config struct {
	RetryCount       int           `yaml:"retryCount" mapstructure:"retryCount"`
	RetryWaitTime    time.Duration `yaml:"retryWaitTime" mapstructure:"retryWaitTime"`
	RetryMaxWaitTime time.Duration `yaml:"retryMaxWaitTime" mapstructure:"retryMaxWaitTime"`
	Timeout          time.Duration `yaml:"timeout" mapstructure:"timeout"`

	// OpenTelemetry Configuration (optional - nil disables telemetry)
	OTelConfig *otel.Config `yaml:"-" mapstructure:"-"` // Not serializable from config files
}

Config RestConfig contains configuration for REST client

func DefaultRestConfig

func DefaultRestConfig() *Config

DefaultRestConfig returns a default REST configuration

type DatabaseLoggingMiddleware

type DatabaseLoggingMiddleware struct{}

DatabaseLoggingMiddleware logs HTTP requests and responses to a database (placeholder implementation)

func NewDatabaseLoggingMiddleware

func NewDatabaseLoggingMiddleware() *DatabaseLoggingMiddleware

NewDatabaseLoggingMiddleware creates a new DatabaseLoggingMiddleware instance

func (*DatabaseLoggingMiddleware) AfterRequest

func (m *DatabaseLoggingMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest logs the request to database (placeholder - just logs to console for now)

func (*DatabaseLoggingMiddleware) BeforeRequest

func (m *DatabaseLoggingMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest stores the start time in context for later use

type ExecutionError

type ExecutionError struct {
	Msg string
	Err error
}

ExecutionError represents an error during execution

func NewExecutionError

func NewExecutionError(msg string, err error) *ExecutionError

func (*ExecutionError) Error

func (e *ExecutionError) Error() string

func (*ExecutionError) Unwrap

func (e *ExecutionError) Unwrap() error

type LoggingMiddleware

type LoggingMiddleware struct{}

LoggingMiddleware logs HTTP requests and responses

func NewLoggingMiddleware

func NewLoggingMiddleware() *LoggingMiddleware

NewLoggingMiddleware creates a new LoggingMiddleware instance

func (*LoggingMiddleware) AfterRequest

func (m *LoggingMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest logs the completion of the request with timing information

func (*LoggingMiddleware) BeforeRequest

func (m *LoggingMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest logs the start of the request and stores the start time in context

type Middleware

type Middleware interface {
	BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context
	AfterRequest(ctx context.Context, info RequestInfo)
}

type NoOpMiddleware

type NoOpMiddleware struct{}

NoOpMiddleware is a middleware that does nothing - useful for testing and as a placeholder

func NewNoOpMiddleware

func NewNoOpMiddleware() *NoOpMiddleware

NewNoOpMiddleware creates a new NoOpMiddleware instance

func (*NoOpMiddleware) AfterRequest

func (m *NoOpMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest does nothing

func (*NoOpMiddleware) BeforeRequest

func (m *NoOpMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest does nothing and returns the context unchanged

type OTelLoggingMiddleware

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

OTelLoggingMiddleware implements structured logging with trace correlation for HTTP client

func NewOTelLoggingMiddleware

func NewOTelLoggingMiddleware(cfg *pkgotel.Config) *OTelLoggingMiddleware

NewOTelLoggingMiddleware creates a new OpenTelemetry logging middleware

func (*OTelLoggingMiddleware) AfterRequest

func (m *OTelLoggingMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest logs the completion of the request with trace correlation

func (*OTelLoggingMiddleware) BeforeRequest

func (m *OTelLoggingMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest logs the start of the request

type OTelMetricsMiddleware

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

OTelMetricsMiddleware implements metrics collection for HTTP client requests

func NewOTelMetricsMiddleware

func NewOTelMetricsMiddleware(cfg *pkgotel.Config) *OTelMetricsMiddleware

NewOTelMetricsMiddleware creates a new OpenTelemetry metrics middleware

func (*OTelMetricsMiddleware) AfterRequest

func (m *OTelMetricsMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest records response metrics

func (*OTelMetricsMiddleware) BeforeRequest

func (m *OTelMetricsMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest records request size metrics

func (*OTelMetricsMiddleware) RecordRetry

func (m *OTelMetricsMiddleware) RecordRetry(ctx context.Context, method string, attempt int)

RecordRetry records a retry attempt (to be called by retry logic)

type OTelTracingMiddleware

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

OTelTracingMiddleware implements distributed tracing for HTTP client requests

func NewOTelTracingMiddleware

func NewOTelTracingMiddleware(cfg *pkgotel.Config) *OTelTracingMiddleware

NewOTelTracingMiddleware creates a new OpenTelemetry tracing middleware

func (*OTelTracingMiddleware) AfterRequest

func (m *OTelTracingMiddleware) AfterRequest(ctx context.Context, info RequestInfo)

AfterRequest ends the span and records the response status

func (*OTelTracingMiddleware) BeforeRequest

func (m *OTelTracingMiddleware) BeforeRequest(ctx context.Context, method string, url string, body string, headers map[string]string) context.Context

BeforeRequest starts a new span for the HTTP request and injects trace context into headers

type RequestInfo

type RequestInfo struct {
	Method     string
	URL        string
	Headers    map[string]string
	Body       string
	StartTime  time.Time
	EndTime    time.Time
	Duration   time.Duration
	StatusCode int
	Response   string
	Error      error
	TraceInfo  resty.TraceInfo
}

type ResourceNotFoundError

type ResourceNotFoundError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

func NewResourceNotFoundError

func NewResourceNotFoundError(statusCode int, msg string, respBody string) *ResourceNotFoundError

func (*ResourceNotFoundError) Error

func (e *ResourceNotFoundError) Error() string

type ResponseError

type ResponseError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

func NewResponseError

func NewResponseError(statusCode int, msg string, respBody string) *ResponseError

func (*ResponseError) Error

func (e *ResponseError) Error() string

type ServerError

type ServerError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

func NewServerError

func NewServerError(statusCode int, msg string, respBody string) *ServerError

NewServerError creates a new ServerError

func (*ServerError) Error

func (e *ServerError) Error() string

type UnauthorizedError

type UnauthorizedError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

UnauthorizedError represents an unauthorized error (HTTP 401)

func NewUnauthorizedError

func NewUnauthorizedError(statusCode int, msg string, respBody string) *UnauthorizedError

NewUnauthorizedError creates a new UnauthorizedError

func (*UnauthorizedError) Error

func (e *UnauthorizedError) Error() string

Jump to

Keyboard shortcuts

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