rest

package
v2.7.14 Latest Latest
Warning

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

Go to latest
Published: Feb 25, 2026 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

Overview

Package rest provides an HTTP client with middleware support, retry logic, and optional OpenTelemetry instrumentation.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrUnauthorized     = errors.New("unauthorized")
	ErrResourceNotFound = errors.New("resource not found")
	ErrServer           = errors.New("server error")
	ErrResponse         = errors.New("response error")
)

Sentinel errors for use with errors.Is.

Functions

func IsClientError

func IsClientError(response *resty.Response) bool

IsClientError returns true for any HTTP 4xx status code. Note: this overlaps with IsUnauthorized and IsNotFound; in HandleResponse, those are checked first so IsClientError only catches remaining 4xx codes.

func IsForbidden added in v2.7.13

func IsForbidden(response *resty.Response) bool

IsForbidden returns true only for HTTP 403 (Forbidden).

func IsNotFound

func IsNotFound(response *resty.Response) bool

IsNotFound returns true for HTTP 404 (Not Found).

func IsServerError

func IsServerError(response *resty.Response) bool

IsServerError returns true for HTTP 5xx status codes.

func IsUnauthorized

func IsUnauthorized(response *resty.Response) bool

IsUnauthorized returns true for HTTP 401 (Unauthorized) and 403 (Forbidden). Both indicate an access control failure; use response.StatusCode() to distinguish them.

Types

type Client

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

Client wraps a resty HTTP client with middleware and OTel support.

func NewClient

func NewClient(options ...ClientOption) *Client

NewClient creates a new REST client with the given options.

func (*Client) AddMiddleware

func (c *Client) AddMiddleware(middleware Middleware)

AddMiddleware appends a middleware to the chain.

func (*Client) GetMiddlewares

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

GetMiddlewares returns a copy of the current middleware chain.

func (*Client) GetRestClient

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

GetRestClient returns the underlying resty client.

func (*Client) GetRestConfig

func (c *Client) GetRestConfig() *Config

GetRestConfig returns a copy of the current REST configuration.

func (*Client) HandleResponse

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

HandleResponse checks the HTTP status code and returns a typed error for non-success responses. Checks are ordered from most specific to least: 401/403 -> 404 -> 5xx -> other 4xx.

func (*Client) MakeRequest

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

MakeRequest executes an HTTP request without resty trace.

The body parameter is a string; for binary payloads, use GetRestClient() and build the request directly with resty's SetBody(interface{}).

func (*Client) MakeRequestWithTrace

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

MakeRequestWithTrace executes an HTTP request with resty trace enabled.

The body parameter is a string; for binary payloads, use GetRestClient() and build the request directly with resty's SetBody(interface{}).

func (*Client) SetMiddlewares

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

SetMiddlewares replaces the entire middleware chain.

type ClientOption

type ClientOption func(*Client)

ClientOption configures a Client during construction.

func WithMiddleware

func WithMiddleware(middleware Middleware) ClientOption

WithMiddleware appends a single middleware to the existing middleware chain.

func WithMiddlewares

func WithMiddlewares(middlewares ...Middleware) ClientOption

WithMiddlewares replaces the entire middleware chain with the provided middlewares. Use WithMiddleware to append instead.

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

WithRestConfig sets the REST client configuration.

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 holds configuration for the REST client.

func DefaultRestConfig

func DefaultRestConfig() *Config

DefaultRestConfig returns a default REST configuration with sensible defaults.

type ExecutionError

type ExecutionError struct {
	Msg string
	Err error
}

ExecutionError represents an error during request execution (e.g. network failure).

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 returns the context unchanged; timing is handled via RequestInfo.

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
}

ResourceNotFoundError represents a 404 Not Found response.

func NewResourceNotFoundError

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

func (*ResourceNotFoundError) Error

func (e *ResourceNotFoundError) Error() string

func (*ResourceNotFoundError) Unwrap added in v2.7.13

func (e *ResourceNotFoundError) Unwrap() error

type ResponseError

type ResponseError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

ResponseError represents a client-side HTTP error (HTTP 4xx, excluding 401/403/404).

func NewResponseError

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

func (*ResponseError) Error

func (e *ResponseError) Error() string

func (*ResponseError) Unwrap added in v2.7.13

func (e *ResponseError) Unwrap() error

type ServerError

type ServerError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

ServerError represents a server-side failure (HTTP 5xx).

func NewServerError

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

NewServerError creates a new ServerError

func (*ServerError) Error

func (e *ServerError) Error() string

func (*ServerError) Unwrap added in v2.7.13

func (e *ServerError) Unwrap() error

type UnauthorizedError

type UnauthorizedError struct {
	StatusCode int
	Msg        string
	RespBody   string
}

UnauthorizedError represents an authentication or authorization failure (HTTP 401/403).

func NewUnauthorizedError

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

NewUnauthorizedError creates a new UnauthorizedError

func (*UnauthorizedError) Error

func (e *UnauthorizedError) Error() string

func (*UnauthorizedError) Unwrap added in v2.7.13

func (e *UnauthorizedError) Unwrap() error

Jump to

Keyboard shortcuts

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