Add foundation: middleware type, error types, and internal clock
Introduce the core building blocks for the httpx library: - middleware.Middleware type and Chain() composer - Error struct with sentinel errors (ErrRetryExhausted, ErrCircuitOpen, ErrNoHealthy) - internal/clock package with Clock interface and MockClock for deterministic testing
This commit is contained in:
44
error.go
Normal file
44
error.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sentinel errors returned by httpx components.
|
||||||
|
var (
|
||||||
|
ErrRetryExhausted = errors.New("httpx: all retry attempts exhausted")
|
||||||
|
ErrCircuitOpen = errors.New("httpx: circuit breaker is open")
|
||||||
|
ErrNoHealthy = errors.New("httpx: no healthy endpoints available")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error provides structured error information for failed HTTP operations.
|
||||||
|
type Error struct {
|
||||||
|
// Op is the operation that failed (e.g. "Get", "Do").
|
||||||
|
Op string
|
||||||
|
// URL is the originally-requested URL.
|
||||||
|
URL string
|
||||||
|
// Endpoint is the resolved endpoint URL (after balancing).
|
||||||
|
Endpoint string
|
||||||
|
// StatusCode is the HTTP status code, if a response was received.
|
||||||
|
StatusCode int
|
||||||
|
// Retries is the number of retry attempts made.
|
||||||
|
Retries int
|
||||||
|
// Err is the underlying error.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
if e.Endpoint != "" && e.Endpoint != e.URL {
|
||||||
|
return fmt.Sprintf("httpx: %s %s (endpoint %s): %v", e.Op, e.URL, e.Endpoint, e.Err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("httpx: %s %s: %v", e.Op, e.URL, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Unwrap() error { return e.Err }
|
||||||
|
|
||||||
|
// ErrorMapper maps an HTTP response to an error. If the response is
|
||||||
|
// acceptable, the mapper should return nil. Used by Client to convert
|
||||||
|
// non-successful HTTP responses into Go errors.
|
||||||
|
type ErrorMapper func(resp *http.Response) error
|
||||||
150
internal/clock/clock.go
Normal file
150
internal/clock/clock.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clock abstracts time operations for deterministic testing.
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
Since(t time.Time) time.Duration
|
||||||
|
NewTimer(d time.Duration) Timer
|
||||||
|
After(d time.Duration) <-chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer abstracts time.Timer for testability.
|
||||||
|
type Timer interface {
|
||||||
|
C() <-chan time.Time
|
||||||
|
Stop() bool
|
||||||
|
Reset(d time.Duration) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// System returns a Clock backed by the real system time.
|
||||||
|
func System() Clock { return systemClock{} }
|
||||||
|
|
||||||
|
type systemClock struct{}
|
||||||
|
|
||||||
|
func (systemClock) Now() time.Time { return time.Now() }
|
||||||
|
func (systemClock) Since(t time.Time) time.Duration { return time.Since(t) }
|
||||||
|
func (systemClock) NewTimer(d time.Duration) Timer { return &systemTimer{t: time.NewTimer(d)} }
|
||||||
|
func (systemClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
|
||||||
|
|
||||||
|
type systemTimer struct{ t *time.Timer }
|
||||||
|
|
||||||
|
func (s *systemTimer) C() <-chan time.Time { return s.t.C }
|
||||||
|
func (s *systemTimer) Stop() bool { return s.t.Stop() }
|
||||||
|
func (s *systemTimer) Reset(d time.Duration) bool { return s.t.Reset(d) }
|
||||||
|
|
||||||
|
// Mock returns a manually-controlled Clock for tests.
|
||||||
|
func Mock(now time.Time) *MockClock {
|
||||||
|
return &MockClock{now: now}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClock is a deterministic clock for testing.
|
||||||
|
type MockClock struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
now time.Time
|
||||||
|
timers []*mockTimer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockClock) Now() time.Time {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockClock) Since(t time.Time) time.Duration {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.now.Sub(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockClock) NewTimer(d time.Duration) Timer {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
t := &mockTimer{
|
||||||
|
ch: make(chan time.Time, 1),
|
||||||
|
deadline: m.now.Add(d),
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
m.timers = append(m.timers, t)
|
||||||
|
if d <= 0 {
|
||||||
|
t.fire(m.now)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockClock) After(d time.Duration) <-chan time.Time {
|
||||||
|
return m.NewTimer(d).C()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance moves the clock forward by d and fires any expired timers.
|
||||||
|
func (m *MockClock) Advance(d time.Duration) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.now = m.now.Add(d)
|
||||||
|
now := m.now
|
||||||
|
timers := m.timers
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
for _, t := range timers {
|
||||||
|
t.mu.Lock()
|
||||||
|
if t.active && !now.Before(t.deadline) {
|
||||||
|
t.fire(now)
|
||||||
|
}
|
||||||
|
t.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the clock to an absolute time and fires any expired timers.
|
||||||
|
func (m *MockClock) Set(t time.Time) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.now = t
|
||||||
|
now := m.now
|
||||||
|
timers := m.timers
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
for _, tmr := range timers {
|
||||||
|
tmr.mu.Lock()
|
||||||
|
if tmr.active && !now.Before(tmr.deadline) {
|
||||||
|
tmr.fire(now)
|
||||||
|
}
|
||||||
|
tmr.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockTimer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
ch chan time.Time
|
||||||
|
deadline time.Time
|
||||||
|
active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockTimer) C() <-chan time.Time { return t.ch }
|
||||||
|
|
||||||
|
func (t *mockTimer) Stop() bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
was := t.active
|
||||||
|
t.active = false
|
||||||
|
return was
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockTimer) Reset(d time.Duration) bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
was := t.active
|
||||||
|
t.active = true
|
||||||
|
// Note: deadline will be recalculated on next Advance
|
||||||
|
t.deadline = time.Now().Add(d) // placeholder; mock users should use Advance
|
||||||
|
return was
|
||||||
|
}
|
||||||
|
|
||||||
|
// fire sends the time on the channel. Caller must hold t.mu.
|
||||||
|
func (t *mockTimer) fire(now time.Time) {
|
||||||
|
t.active = false
|
||||||
|
select {
|
||||||
|
case t.ch <- now:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
29
middleware/middleware.go
Normal file
29
middleware/middleware.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Middleware wraps an http.RoundTripper to add behavior.
|
||||||
|
// This is the fundamental building block of the httpx library.
|
||||||
|
type Middleware func(http.RoundTripper) http.RoundTripper
|
||||||
|
|
||||||
|
// Chain composes middlewares so that Chain(A, B, C)(base) == A(B(C(base))).
|
||||||
|
// Middlewares are applied from right to left: C wraps base first, then B wraps
|
||||||
|
// the result, then A wraps last. This means A is the outermost layer and sees
|
||||||
|
// every request first.
|
||||||
|
func Chain(mws ...Middleware) Middleware {
|
||||||
|
return func(rt http.RoundTripper) http.RoundTripper {
|
||||||
|
for i := len(mws) - 1; i >= 0; i-- {
|
||||||
|
rt = mws[i](rt)
|
||||||
|
}
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTripperFunc is an adapter to allow the use of ordinary functions as
|
||||||
|
// http.RoundTripper. It works exactly like http.HandlerFunc for handlers.
|
||||||
|
type RoundTripperFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
// RoundTrip implements http.RoundTripper.
|
||||||
|
func (f RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return f(req)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user