http Package
HTTP client for inter-service communication with retry logic, timeouts, and automatic logging.
Features
- Automatic Retry: Configurable retry attempts for failed requests
- Timeout Management: Request-level timeout configuration
- JSON Support: Automatic JSON serialization/deserialization
- Custom Headers: Easy header management (Authorization, etc.)
- Logging Integration: Automatic request/response logging with duration
- Error Handling: Smart retry logic (no retry on 4xx errors)
- Context Support: Cancellation and timeout propagation
Installation
import "github.com/LaRestoOU/laresto-go-common/pkg/http"
Quick Start
Create Client
cfg := http.Config{
Timeout: 30 * time.Second,
MaxRetries: 3,
RetryDelay: 1 * time.Second,
}
log := logger.New(logger.Config{...})
client := http.NewClient(cfg, log)
GET Request
ctx := context.Background()
resp, err := client.Get(ctx, "https://api.example.com/users/123", nil)
if err != nil {
return err
}
var user User
if err := resp.JSON(&user); err != nil {
return err
}
POST Request
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
req := CreateUserRequest{
Email: "user@example.com",
Name: "John Doe",
}
resp, err := client.Post(ctx, "https://api.example.com/users", req, nil)
if err != nil {
return err
}
headers := map[string]string{
"Authorization": "Bearer " + token,
"X-Request-ID": requestID,
}
resp, err := client.Get(ctx, url, headers)
Configuration
type Config struct {
// Timeout is the request timeout (default: 30s)
Timeout time.Duration
// MaxRetries is retry attempts (default: 3)
MaxRetries int
// RetryDelay is delay between retries (default: 1s)
RetryDelay time.Duration
// UserAgent is the User-Agent header (default: "laresto-http-client/1.0")
UserAgent string
}
Methods
GET
resp, err := client.Get(ctx, url, headers)
POST
resp, err := client.Post(ctx, url, body, headers)
PUT
resp, err := client.Put(ctx, url, body, headers)
DELETE
resp, err := client.Delete(ctx, url, headers)
Generic Do
resp, err := client.Do(ctx, "PATCH", url, body, headers)
Response Handling
// Get response
resp, err := client.Get(ctx, url, nil)
// Parse JSON
var result MyType
if err := resp.JSON(&result); err != nil {
return err
}
// Get raw string
text := resp.String()
// Access status code
statusCode := resp.StatusCode
// Access headers
contentType := resp.Headers.Get("Content-Type")
Usage Patterns
Service-to-Service Communication
type UserServiceClient struct {
httpClient *http.Client
baseURL string
}
func (c *UserServiceClient) GetUser(ctx context.Context, userID string) (*User, error) {
url := fmt.Sprintf("%s/users/%s", c.baseURL, userID)
resp, err := c.httpClient.Get(ctx, url, nil)
if err != nil {
return nil, err
}
var user User
if err := resp.JSON(&user); err != nil {
return nil, err
}
return &user, nil
}
func (c *UserServiceClient) CreateUser(ctx context.Context, req CreateUserRequest) (*User, error) {
url := fmt.Sprintf("%s/users", c.baseURL)
resp, err := c.httpClient.Post(ctx, url, req, nil)
if err != nil {
return nil, err
}
var user User
if err := resp.JSON(&user); err != nil {
return nil, err
}
return &user, nil
}
Authenticated Requests
type AuthenticatedClient struct {
httpClient *http.Client
token string
}
func (c *AuthenticatedClient) makeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
headers := map[string]string{
"Authorization": "Bearer " + c.token,
}
return c.httpClient.Do(ctx, method, url, body, headers)
}
func (c *AuthenticatedClient) GetProfile(ctx context.Context) (*Profile, error) {
resp, err := c.makeRequest(ctx, "GET", "/api/profile", nil)
if err != nil {
return nil, err
}
var profile Profile
if err := resp.JSON(&profile); err != nil {
return nil, err
}
return &profile, nil
}
Circuit Breaker Pattern
type ResilientClient struct {
httpClient *http.Client
failures int
threshold int
isOpen bool
}
func (c *ResilientClient) Call(ctx context.Context, url string) error {
if c.isOpen {
return errors.New("CIRCUIT_OPEN", "Circuit breaker is open", 503)
}
_, err := c.httpClient.Get(ctx, url, nil)
if err != nil {
c.failures++
if c.failures >= c.threshold {
c.isOpen = true
}
return err
}
c.failures = 0
return nil
}
Retry Logic
Automatic retry on:
- 5xx server errors
- Network errors
- Timeouts
No retry on:
- 4xx client errors (bad request, unauthorized, not found, etc.)
- Context cancellation
- Successfully completed requests
// This will retry up to 3 times
cfg := http.Config{
MaxRetries: 3,
RetryDelay: 1 * time.Second,
}
client := http.NewClient(cfg, log)
// 500 error -> retries
resp, err := client.Get(ctx, url, nil)
// 404 error -> no retry (client error)
resp, err := client.Get(ctx, url, nil)
Timeout Handling
// Request-level timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.Get(ctx, url, nil)
// Client-level timeout
cfg := http.Config{
Timeout: 30 * time.Second, // All requests timeout after 30s
}
client := http.NewClient(cfg, log)
Best Practices
DO ✅
// Always use context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Check errors
resp, err := client.Get(ctx, url, nil)
if err != nil {
return err
}
// Use appropriate HTTP methods
client.Get(ctx, url, nil) // Read
client.Post(ctx, url, body, nil) // Create
client.Put(ctx, url, body, nil) // Update
client.Delete(ctx, url, nil) // Delete
// Include request IDs for tracing
headers := map[string]string{
"X-Request-ID": requestID,
}
DON'T ❌
// Don't use without timeout
ctx := context.Background() // No timeout!
// Don't ignore errors
client.Get(ctx, url, nil) // Error ignored!
// Don't use wrong HTTP methods
client.Get(ctx, url, body, nil) // GET with body!
// Don't retry on 4xx errors manually
// Library handles this automatically
Error Handling
resp, err := client.Get(ctx, url, nil)
if err != nil {
// Check error type
if ctx.Err() == context.DeadlineExceeded {
// Timeout
log.Error("Request timeout")
} else {
// Other error (network, server, etc.)
log.Error("Request failed", err)
}
return err
}
// Check status code
if resp.StatusCode >= 400 {
// Handle error response
var errResp ErrorResponse
resp.JSON(&errResp)
return fmt.Errorf("API error: %s", errResp.Message)
}
Testing
Mock Server
func TestClient(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id":"123"}`))
}))
defer server.Close()
// Test client
client := http.NewClient(http.Config{}, logger.NewDefault())
resp, err := client.Get(context.Background(), server.URL, nil)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
- Reuse clients: Create once, use many times
- Set appropriate timeouts: Balance reliability vs responsiveness
- Configure retries wisely: Too many = slow, too few = unreliable
- Use connection pooling: Handled automatically by http.Client
Security
// ✅ GOOD: Use HTTPS
client.Get(ctx, "https://api.example.com/data", nil)
// ❌ BAD: Use HTTP for sensitive data
client.Get(ctx, "http://api.example.com/data", nil)
// ✅ GOOD: Include auth headers
headers := map[string]string{
"Authorization": "Bearer " + token,
}
// ✅ GOOD: Validate TLS certificates (automatic)
License
MIT License - see LICENSE file for details