Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./go.mod

Expand Down Expand Up @@ -80,4 +80,4 @@ jobs:
# execute again to get the summary
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Coverage report" >> $GITHUB_STEP_SUMMARY
go-test-coverage --config=./.testcoverage.yml | sed 's/PASS/PASS ✅/g' | sed 's/FAIL/FAIL ❌/g' | tee -a $GITHUB_STEP_SUMMARY
go-test-coverage --config=./.testcoverage.yml | sed 's/PASS/PASS ✅/g' | sed 's/FAIL/FAIL ❌/g' | tee -a $GITHUB_STEP_SUMMARY
7 changes: 3 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name: Release

# https://help.github.com/es/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
Expand All @@ -19,11 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Go 1.x
id: go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: ./go.mod

Expand Down Expand Up @@ -71,4 +70,4 @@ jobs:
draft: false
prerelease: false
generate_release_notes: true
make_latest: true
make_latest: true
233 changes: 229 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/p2p-b2b/httpretrier.svg)](https://pkg.go.dev/github.com/p2p-b2b/httpretrier)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/p2p-b2b/httpretrier?style=plastic)

`httpretrier` is a Go library that provides a convenient way to add automatic retry logic to your HTTP requests. It wraps the standard `http.Client` and `http.Transport` to handle transient server errors (5xx) or network issues by retrying requests based on configurable strategies.
`httpretrier` is a Go library that provides a **transparent** drop-in replacement for `http.Client` with automatic retry logic. It preserves all existing request headers (including authentication) while handling transient server errors (5xx) or network issues by retrying requests based on configurable strategies.

## Features

* **Transparent by Default:** Works as a zero-configuration drop-in replacement for `http.Client`, automatically preserving existing authentication tokens, custom headers, and all request properties without any code changes.
* **Automatic Retries:** Automatically retries requests that fail due to server errors (5xx) or transport-level errors.
* **Configurable Retry Strategies:**
* `FixedDelay`: Retries after a constant delay.
Expand All @@ -17,7 +18,7 @@
* Base and maximum delay for backoff strategies.
* Standard `http.Transport` settings (timeouts, keep-alives, connection pooling).
* Overall request timeout (`http.Client.Timeout`).
* **Easy Integration:** Designed as a drop-in replacement for `http.Client`.
* **Easy Integration:** Designed as a complete drop-in replacement for `http.Client` - just change your client creation line and everything else works transparently.

## Installation

Expand All @@ -27,9 +28,26 @@ go get github.com/p2p-b2b/httpretrier

## Usage

### Basic Usage with Default Transport
### Basic Transparent Usage (Recommended)

You can quickly create a client with a specific retry strategy and number of retries using `httpretrier.NewClient`. It uses `http.DefaultTransport` underneath.
The easiest way to add retry functionality is to replace your `http.Client` with `httpretrier.NewClient()`. It works transparently with all existing headers and authentication tokens.

```go
// Before: client := &http.Client{}
client := httpretrier.NewClient(3, httpretrier.ExponentialBackoff(500*time.Millisecond, 10*time.Second), nil)

// All your existing code works unchanged - auth tokens, custom headers, everything is preserved
req, _ := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer your-existing-token")
req.Header.Set("X-Custom-Header", "your-value")

resp, err := client.Do(req)
// Automatically retries on 5xx errors while preserving all headers
```

### Basic Usage with Specific Configuration

You can create a client with specific retry strategy and number of retries using `httpretrier.NewClient`.

```go
package main
Expand Down Expand Up @@ -88,6 +106,52 @@ func main() {
// Client: Received response: Status=200 OK, Body='Success!'
```

### Using Custom Transport

You can provide your own `http.Transport` with specific settings for connection pooling, timeouts, TLS configuration, etc.:

```go
package main

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

"github.com/p2p-b2b/httpretrier"
)

func main() {
// Create a custom transport with specific settings
customTransport := &http.Transport{
MaxIdleConns: 50, // Custom connection pool size
IdleConnTimeout: 30 * time.Second, // Custom idle timeout
DisableKeepAlives: false, // Enable keep-alives
MaxIdleConnsPerHost: 10, // Custom per-host connection limit
TLSHandshakeTimeout: 5 * time.Second, // Custom TLS timeout
}

// Create retry client with your custom transport
client := httpretrier.NewClient(
3, // Max retries
httpretrier.ExponentialBackoff(100*time.Millisecond, 1*time.Second),
customTransport, // Use your custom transport as the base
)

// Use the client normally - all your transport settings are preserved
resp, err := client.Get("https://api.example.com/data")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()

// Your custom transport settings (connection pooling, timeouts) are used
// while still getting automatic retry functionality
fmt.Printf("Success with custom transport! Status: %d\n", resp.StatusCode)
}
```

### Advanced Configuration with ClientBuilder

For more control over the client and transport settings, use the `ClientBuilder`.
Expand Down Expand Up @@ -143,10 +207,171 @@ func main() {
// Client (Builder): Received response: Status=200 OK, Body='Builder success!'
```

## Authorization

The library provides built-in support for common HTTP authentication patterns. Authorization is applied to all requests, including retries, and automatically handles 401 Unauthorized responses by attempting to refresh credentials when supported.

### Bearer Token Authentication

```go
// Simple Bearer token
client := httpretrier.NewClientBuilder().
WithBearerToken("your-access-token").
WithMaxRetries(3).
Build()

// Bearer token with automatic refresh
client := httpretrier.NewClientBuilder().
WithBearerTokenAndRefresh("initial-token", func() (string, error) {
// Your token refresh logic here
return refreshTokenFromAPI()
}).
WithMaxRetries(3).
Build()
```

### API Key Authentication

```go
// API key in custom header
client := httpretrier.NewClientBuilder().
WithAPIKey("your-api-key", "X-API-Key").
WithMaxRetries(3).
Build()
```

### Basic Authentication

```go
client := httpretrier.NewClientBuilder().
WithBasicAuth("username", "password").
WithMaxRetries(3).
Build()
```

### Custom Header Authentication

```go
// Multiple custom headers
headers := map[string]string{
"X-Client-ID": "your-client-id",
"X-Signature": "your-hmac-signature",
}

client := httpretrier.NewClientBuilder().
WithCustomHeaders(headers).
WithMaxRetries(3).
Build()
```

### Custom Authorizer

For advanced authentication schemes, implement the `Authorizer` interface:

```go
type MyCustomAuth struct {
// Your auth fields
}

func (a *MyCustomAuth) Authorize(req *http.Request) error {
// Add your custom authorization logic
req.Header.Set("Authorization", "Custom "+a.Token)
return nil
}

func (a *MyCustomAuth) RefreshIfNeeded() error {
// Optional: refresh logic for 401 responses
return nil
}

// Use custom authorizer
client := httpretrier.NewClientBuilder().
WithAuthorizer(&MyCustomAuth{}).
WithMaxRetries(3).
Build()
```

### Request-Level Authorization

For scenarios where Bearer tokens are already present in requests (like proxy servers, middleware, or request forwarding):

```go
// Use Bearer tokens from incoming requests
client := httpretrier.NewClientBuilder().
WithRequestTokenAuth(false). // false = require token, true = allow empty
WithMaxRetries(3).
Build()

// With automatic token refresh on 401
client := httpretrier.NewClientBuilder().
WithRequestTokenAuthAndRefresh(func(currentToken string) (string, error) {
// Refresh the token based on current token
return refreshToken(currentToken)
}, false).
WithMaxRetries(3).
Build()

// Make request with existing Authorization header
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
req.Header.Set("Authorization", "Bearer existing-token")
resp, err := client.Do(req)
```

### Passthrough Authorization

Preserve existing authorization headers and optionally provide fallback authentication:

```go
// Preserve existing auth, use fallback if none exists
client := httpretrier.NewClientBuilder().
WithPassthroughAuth(NewBearerTokenAuth("fallback-token")).
Build()
```

### Conditional Authorization

Apply different authentication strategies based on request context:

```go
client := httpretrier.NewClientBuilder().
WithConditionalAuth(func(req *http.Request) Authorizer {
service := req.Header.Get("X-Service")
switch service {
case "internal":
return NewBearerTokenAuth("internal-token")
case "external":
return NewAPIKeyAuth("external-key", "X-API-Key")
default:
return nil // No auth
}
}).
Build()
```

### Authorization with Retry Integration

Authorization works seamlessly with retry logic:

1. **Auth + Retry**: Authorization headers are added to each retry attempt
2. **401 Handling**: On 401 Unauthorized responses, the authorizer attempts to refresh credentials
3. **Automatic Retry**: After successful credential refresh, the request is automatically retried once
4. **Layered Approach**: Auth transport wraps retry transport, ensuring proper order of operations

## Configuration Options (ClientBuilder)

The `ClientBuilder` allows configuration of:

* **Authorization:**
* `WithBearerToken(token string)`: Add Bearer token authentication.
* `WithBearerTokenAndRefresh(token string, refreshFunc func() (string, error))`: Bearer token with refresh capability.
* `WithAPIKey(key, header string)`: Add API key authentication in specified header.
* `WithBasicAuth(username, password string)`: Add HTTP Basic authentication.
* `WithCustomHeaders(headers map[string]string)`: Add custom header authentication.
* `WithRequestTokenAuth(allowEmpty bool)`: Use Bearer tokens from incoming requests.
* `WithRequestTokenAuthAndRefresh(refreshFunc func(string) (string, error), allowEmpty bool)`: Request-level tokens with refresh.
* `WithPassthroughAuth(defaultAuth Authorizer)`: Preserve existing auth, use default if none.
* `WithConditionalAuth(condition func(*http.Request) Authorizer)`: Context-aware authorization.
* `WithAuthorizer(authorizer Authorizer)`: Use a custom authorizer implementation.
* **Retry Logic:**
* `WithMaxRetries(int)`: Maximum number of retry attempts.
* `WithRetryStrategy(httpretrier.Strategy)`: Set the strategy (`FixedDelayStrategy`, `ExponentialBackoffStrategy`, `JitterBackoffStrategy`).
Expand Down
Loading