Documentation Index
Fetch the complete documentation index at: https://mintlify.com/bogdanfinn/tls-client/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Hooks allow you to execute custom logic at specific points in the request lifecycle:
- Pre-request hooks run before each request is sent
- Post-response hooks run after each request completes (success or failure)
Hooks are useful for logging, metrics collection, request modification, response validation, and implementing custom retry logic.
Type definitions
PreRequestHookFunc
type PreRequestHookFunc func(req *http.Request) error
Called before each request is sent. Return an error to abort the request.
PostResponseHookFunc
type PostResponseHookFunc func(ctx *PostResponseContext) error
Called after each request completes, regardless of success or failure.
PostResponseContext
type PostResponseContext struct {
Request *http.Request
Response *http.Response
Error error // Non-nil if request failed
}
Contains the request, response, and any error that occurred.
Error handling
ErrContinueHooks
var ErrContinueHooks = errors.New("continue hooks")
Return (or wrap) ErrContinueHooks from a hook to log the error but continue executing subsequent hooks:
func myHook(req *http.Request) error {
if err := doSomething(); err != nil {
// Log error but continue to next hook
return fmt.Errorf("%w: failed to do something: %v", tls_client.ErrContinueHooks, err)
}
return nil
}
Default behavior: Any error returned from a hook aborts subsequent hooks and the request (for pre-hooks) or stops hook execution (for post-hooks).
Adding hooks
At client creation
Add hooks when creating the client:
client, err := tls_client.NewHttpClient(logger,
tls_client.WithPreHook(myPreHook),
tls_client.WithPostHook(myPostHook),
)
After client creation
Add hooks dynamically using the client methods:
client.AddPreRequestHook(myPreHook)
client.AddPostResponseHook(myPostHook)
Resetting hooks
Remove all hooks:
client.ResetPreHooks()
client.ResetPostHooks()
Execution behavior
Pre-request hooks
- Execute in the order they were added
- Run before the HTTP request is sent
- If any hook returns an error (without
ErrContinueHooks), the request is aborted
- Hooks can modify the request object
- Panics are caught and converted to errors
Post-response hooks
- Execute in the order they were added
- Always run, even if the request failed
- Receive both the response (if available) and any error
- If any hook returns an error (without
ErrContinueHooks), subsequent hooks are not executed
- Panics are caught and logged
Usage examples
Request logging
Log all outgoing requests:
package main
import (
"fmt"
"time"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
func loggingPreHook(req *http.Request) error {
fmt.Printf("[%s] %s %s\n",
time.Now().Format("15:04:05"),
req.Method,
req.URL.String(),
)
return nil
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPreHook(loggingPreHook),
)
if err != nil {
panic(err)
}
client.Get("https://example.com")
// Output: [14:30:45] GET https://example.com
}
Automatically add headers to all requests:
package main
import (
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
func addAuthHeader(req *http.Request) error {
req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
req.Header.Set("X-Custom-Header", "custom-value")
return nil
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPreHook(addAuthHeader),
)
if err != nil {
panic(err)
}
// All requests will include the custom headers
client.Get("https://api.example.com/data")
}
Request timing
Measure request duration:
package main
import (
"fmt"
"time"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
var requestTimes = make(map[*http.Request]time.Time)
func timingPreHook(req *http.Request) error {
requestTimes[req] = time.Now()
return nil
}
func timingPostHook(ctx *tls_client.PostResponseContext) error {
if startTime, ok := requestTimes[ctx.Request]; ok {
duration := time.Since(startTime)
fmt.Printf("%s took %v\n", ctx.Request.URL.String(), duration)
delete(requestTimes, ctx.Request)
}
return nil
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPreHook(timingPreHook),
tls_client.WithPostHook(timingPostHook),
)
if err != nil {
panic(err)
}
client.Get("https://example.com")
// Output: https://example.com took 245ms
}
Response validation
Validate response status codes:
package main
import (
"fmt"
tls_client "github.com/bogdanfinn/tls-client"
)
func validateStatus(ctx *tls_client.PostResponseContext) error {
if ctx.Error != nil {
return nil // Request already failed
}
if ctx.Response.StatusCode >= 400 {
return fmt.Errorf("request failed with status %d", ctx.Response.StatusCode)
}
return nil
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPostHook(validateStatus),
)
if err != nil {
panic(err)
}
resp, err := client.Get("https://example.com/not-found")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Retry logic
Implement custom retry logic with exponential backoff:
package main
import (
"fmt"
"time"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
func makeRequestWithRetry(client tls_client.HttpClient, url string, maxRetries int) (*http.Response, error) {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
// Exponential backoff
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
fmt.Printf("Retrying after %v (attempt %d/%d)\n", backoff, attempt+1, maxRetries+1)
time.Sleep(backoff)
}
resp, err := client.Get(url)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
lastErr = err
if resp != nil {
resp.Body.Close()
}
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger())
if err != nil {
panic(err)
}
resp, err := makeRequestWithRetry(client, "https://api.example.com/data", 3)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
} else {
defer resp.Body.Close()
fmt.Printf("Success: %d\n", resp.StatusCode)
}
}
Metrics collection
Collect detailed request metrics:
package main
import (
"fmt"
"sync/atomic"
tls_client "github.com/bogdanfinn/tls-client"
)
type Metrics struct {
totalRequests atomic.Int64
successRequests atomic.Int64
failedRequests atomic.Int64
}
func (m *Metrics) recordRequest(ctx *tls_client.PostResponseContext) error {
m.totalRequests.Add(1)
if ctx.Error != nil || (ctx.Response != nil && ctx.Response.StatusCode >= 400) {
m.failedRequests.Add(1)
} else {
m.successRequests.Add(1)
}
return nil
}
func (m *Metrics) Print() {
total := m.totalRequests.Load()
success := m.successRequests.Load()
failed := m.failedRequests.Load()
fmt.Printf("\nMetrics:\n")
fmt.Printf(" Total: %d\n", total)
fmt.Printf(" Success: %d (%.1f%%)\n", success, float64(success)/float64(total)*100)
fmt.Printf(" Failed: %d (%.1f%%)\n", failed, float64(failed)/float64(total)*100)
}
func main() {
metrics := &Metrics{}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPostHook(metrics.recordRequest),
)
if err != nil {
panic(err)
}
// Make some requests
urls := []string{
"https://example.com",
"https://example.com/not-found",
"https://example.com/about",
}
for _, url := range urls {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
}
}
metrics.Print()
}
Request rate limiting
Implement simple rate limiting:
package main
import (
"fmt"
"sync"
"time"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
type RateLimiter struct {
requests int
window time.Duration
maxRequests int
mutex sync.Mutex
windowStart time.Time
requestCount int
}
func NewRateLimiter(maxRequests int, window time.Duration) *RateLimiter {
return &RateLimiter{
maxRequests: maxRequests,
window: window,
windowStart: time.Now(),
}
}
func (rl *RateLimiter) PreHook(req *http.Request) error {
rl.mutex.Lock()
defer rl.mutex.Unlock()
now := time.Now()
// Reset window if expired
if now.Sub(rl.windowStart) >= rl.window {
rl.windowStart = now
rl.requestCount = 0
}
// Check if limit exceeded
if rl.requestCount >= rl.maxRequests {
sleepTime := rl.window - now.Sub(rl.windowStart)
fmt.Printf("Rate limit reached, sleeping for %v\n", sleepTime)
time.Sleep(sleepTime)
// Reset after sleep
rl.windowStart = time.Now()
rl.requestCount = 0
}
rl.requestCount++
return nil
}
func main() {
// Allow max 5 requests per 10 seconds
rateLimiter := NewRateLimiter(5, 10*time.Second)
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPreHook(rateLimiter.PreHook),
)
if err != nil {
panic(err)
}
// Make 10 requests - should trigger rate limiting
for i := 0; i < 10; i++ {
fmt.Printf("Request %d\n", i+1)
resp, err := client.Get("https://example.com")
if err == nil {
resp.Body.Close()
}
}
}
Continue on error
Use ErrContinueHooks to log errors without aborting:
package main
import (
"fmt"
http "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
func optionalValidation(req *http.Request) error {
if req.Header.Get("X-Required-Header") == "" {
// Log warning but continue with request
return fmt.Errorf("%w: missing X-Required-Header", tls_client.ErrContinueHooks)
}
return nil
}
func criticalValidation(req *http.Request) error {
if req.URL.Scheme != "https" {
// Abort request
return fmt.Errorf("only HTTPS requests allowed")
}
return nil
}
func main() {
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
tls_client.WithPreHook(optionalValidation),
tls_client.WithPreHook(criticalValidation),
)
if err != nil {
panic(err)
}
// This will log a warning but proceed
resp, err := client.Get("https://example.com")
if err == nil {
resp.Body.Close()
}
// This will abort due to critical validation
resp, err = client.Get("http://example.com")
if err != nil {
fmt.Printf("Request blocked: %v\n", err)
}
}
Best practices
- Keep hooks lightweight: Hooks execute synchronously and block the request
- Handle errors gracefully: Use
ErrContinueHooks for non-critical errors
- Avoid panics: Hooks catch panics, but they still abort the request
- Thread safety: Use proper synchronization for shared state (see rate limiter example)
- Clean up resources: Store request-specific data in maps and clean up in post-hooks
- Order matters: Hooks execute in the order they were added