Documentation
¶
Overview ¶
Package middleware provides composable foxhound.Middleware implementations for rate limiting, deduplication, depth limiting, and retry logic.
Index ¶
- func Chain(middlewares ...foxhound.Middleware) foxhound.Middleware
- func NewAutoThrottle(cfg AutoThrottleConfig) foxhound.Middleware
- func NewBlockDetector(maxRetries int, baseDelay time.Duration, patterns ...BlockPattern) foxhound.Middleware
- func NewCircuitBreaker(cfg CircuitBreakerConfig) foxhound.Middleware
- func NewConcurrency(perDomain int) foxhound.Middleware
- func NewCookies() foxhound.Middleware
- func NewDedup(opts ...DedupOption) foxhound.Middleware
- func NewDeltaFetch(strategy DeltaStrategy, store DeltaStore, ttl time.Duration) foxhound.Middleware
- func NewDepthLimit(maxDepth int) foxhound.Middleware
- func NewDomainDelay(cfg DomainDelayConfig) foxhound.Middleware
- func NewMetrics(namespace string) foxhound.Middleware
- func NewRateLimit(requestsPerSec float64, burstSize int) foxhound.Middleware
- func NewRedirect(maxRedirects int) foxhound.Middleware
- func NewReferer() foxhound.Middleware
- func NewRetry(maxRetries int, baseDelay time.Duration) foxhound.Middleware
- func NewRobotsTxt(userAgent string) foxhound.Middleware
- type AutoThrottleConfig
- type BlockPattern
- type CircuitBreakerConfig
- type CircuitState
- type DedupOption
- type DeltaStore
- type DeltaStrategy
- type DomainDelayConfig
- type MemoryDeltaStore
- type PersistentCookies
- type RedisDeltaStore
- type SQLiteDeltaStore
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Chain ¶
func Chain(middlewares ...foxhound.Middleware) foxhound.Middleware
Chain composes multiple Middleware values into one. Middlewares are applied outermost-first: the first argument's Wrap runs before subsequent ones, so request processing follows the slice order.
func NewAutoThrottle ¶
func NewAutoThrottle(cfg AutoThrottleConfig) foxhound.Middleware
NewAutoThrottle creates an adaptive throttle middleware.
After each response the delay for that domain is recomputed:
- Normal response: delay = EMA(latency) / TargetConcurrency
- 429 or 503: delay spikes to MaxDelay immediately.
- Delay is clamped to [MinDelay, MaxDelay].
The computed delay is slept before the *next* request to that domain.
func NewBlockDetector ¶
func NewBlockDetector(maxRetries int, baseDelay time.Duration, patterns ...BlockPattern) foxhound.Middleware
NewBlockDetector creates a Middleware that detects soft blocks in responses based on HTTP status codes and body content heuristics. When a block is detected it retries with exponential backoff up to maxRetries additional times.
If no patterns are provided DefaultBlockPatterns() is used.
func NewCircuitBreaker ¶
func NewCircuitBreaker(cfg CircuitBreakerConfig) foxhound.Middleware
NewCircuitBreaker creates a circuit breaker middleware.
The circuit breaker monitors the failure rate per domain using a sliding window. When failures exceed the threshold, the circuit opens and all requests fail immediately for an exponentially increasing duration:
open_duration = min(BaseTimeout * 2^(trips-1), MaxTimeout) * U(0.5, 1.5)
After the open duration, one probe request is allowed (half-open state). If it succeeds, the circuit closes. If it fails, the circuit re-opens with an incremented trip count.
func NewConcurrency ¶
func NewConcurrency(perDomain int) foxhound.Middleware
NewConcurrency returns a Middleware that limits the number of concurrent in-flight requests per target domain. A separate semaphore (buffered channel of size perDomain) is created lazily for each unique domain.
The middleware is intended to sit as the outermost layer in the chain so it caps parallelism before any rate-limit or dedup checks are performed.
func NewCookies ¶
func NewCookies() foxhound.Middleware
NewCookies returns a Middleware that automatically persists and replays HTTP cookies across requests within the same session.
func NewDedup ¶
func NewDedup(opts ...DedupOption) foxhound.Middleware
NewDedup creates a Middleware that skips URLs that have already been fetched.
Canonicalisation rules applied before storing/checking:
- Only scheme + host + path + query are compared (fragment dropped).
- Query parameters are sorted alphabetically.
Duplicate requests return a zero-value Response (StatusCode 0, empty body) without calling the underlying Fetcher.
func NewDeltaFetch ¶
func NewDeltaFetch(strategy DeltaStrategy, store DeltaStore, ttl time.Duration) foxhound.Middleware
NewDeltaFetch returns a Middleware that avoids re-fetching known URLs.
- strategy=DeltaSkipSeen: skip if ever seen.
- strategy=DeltaSkipRecent: skip only if seen within ttl.
Skipped requests return a zero-value Response (StatusCode 0) without calling the underlying Fetcher, matching the behaviour of NewDedup.
func NewDepthLimit ¶
func NewDepthLimit(maxDepth int) foxhound.Middleware
NewDepthLimit creates a Middleware that returns an error for any job with Depth > maxDepth, preventing the underlying Fetcher from being called.
func NewDomainDelay ¶
func NewDomainDelay(cfg DomainDelayConfig) foxhound.Middleware
NewDomainDelay creates a Middleware that enforces per-domain download delays. The delay is enforced as a minimum interval between consecutive requests to the same domain.
Example:
middleware.NewDomainDelay(middleware.DomainDelayConfig{
DefaultDelay: 2 * time.Second,
PerDomain: map[string]time.Duration{
"api.example.com": 500 * time.Millisecond,
},
Randomize: true,
})
func NewMetrics ¶
func NewMetrics(namespace string) foxhound.Middleware
NewMetrics returns a Middleware that records Prometheus metrics.
Three instruments are registered under namespace:
- <namespace>_requests_total{domain, status} — request counter
- <namespace>_request_duration_seconds{domain} — latency histogram
- <namespace>_errors_total{domain, error_type} — error counter
All instruments are registered with the default Prometheus registry via promauto, so they are automatically included in the default /metrics handler.
func NewRateLimit ¶
func NewRateLimit(requestsPerSec float64, burstSize int) foxhound.Middleware
NewRateLimit creates a Middleware that allows requestsPerSec requests per second per domain, with a burst of burstSize tokens. A separate rate.Limiter is created lazily for each unique domain.
func NewRedirect ¶
func NewRedirect(maxRedirects int) foxhound.Middleware
NewRedirect returns a Middleware that follows HTTP redirects.
Up to maxRedirects hops are followed. If the chain exceeds maxRedirects an error is returned. A value of 0 disables redirect following entirely.
func NewReferer ¶
func NewReferer() foxhound.Middleware
NewReferer returns a Middleware that automatically sets the Referer header.
Behaviour:
- If a Referer header is already present on the job, it is left unchanged.
- On the first request to a domain, Referer is set to a Google search URL for that domain, mimicking organic search traffic.
- On subsequent requests to the same domain, Referer is set to the URL of the previous request to that domain.
func NewRetry ¶
func NewRetry(maxRetries int, baseDelay time.Duration) foxhound.Middleware
NewRetry creates a Middleware that retries failed or blocked requests up to maxRetries additional times (i.e. 1 + maxRetries total attempts).
Backoff between attempts is baseDelay * 2^attempt, with ±25 % uniform jitter to spread retry storms. Context cancellation stops retries immediately.
func NewRobotsTxt ¶
func NewRobotsTxt(userAgent string) foxhound.Middleware
NewRobotsTxt returns a Middleware that optionally respects robots.txt.
On the first request to a domain the middleware fetches /robots.txt using a plain http.Client and caches the result. Subsequent requests to the same domain use the cached rules.
If robots.txt cannot be fetched (network error, non-2xx status) all URLs are allowed — conservative fail-open behaviour is safer for scraping than incorrectly blocking pages.
Disallowed URLs return a Response with StatusCode 0 without calling the underlying Fetcher, matching the pattern used by NewDedup and NewDeltaFetch.
Types ¶
type AutoThrottleConfig ¶
type AutoThrottleConfig struct {
// TargetConcurrency is the desired parallel request count per domain.
// The algorithm targets: delay = avgLatency / TargetConcurrency.
TargetConcurrency float64
// InitialDelay is the starting inter-request delay per domain.
InitialDelay time.Duration
// MinDelay is the floor delay; the computed delay will not go below this.
MinDelay time.Duration
// MaxDelay is the ceiling delay. A 429 or 503 response spikes to MaxDelay.
MaxDelay time.Duration
// Alpha is the EMA smoothing factor (default 0.3). Higher values react faster
// to latency changes but are more sensitive to outliers.
Alpha float64
}
AutoThrottleConfig holds tuning parameters for the adaptive throttle.
type BlockPattern ¶
type BlockPattern struct {
// Name is a human-readable label used in log output.
Name string
// StatusCode triggers the pattern when the response matches this code.
// A value of 0 means any status code is eligible.
StatusCode int
// BodyContains lists substrings; if any are found in the lowercased body
// the response is considered blocked.
BodyContains []string
// MinBodySize marks a response as suspicious when the body is smaller than
// this threshold (bytes). 0 disables this check.
MinBodySize int
// MaxBodySize flags a response as blocked when the body exceeds this
// threshold (bytes). 0 disables this check.
MaxBodySize int
}
BlockPattern defines a pattern that indicates a blocked response.
func DefaultBlockPatterns ¶
func DefaultBlockPatterns() []BlockPattern
DefaultBlockPatterns returns the standard set of block detection patterns covering the most common anti-bot vendors and generic block signals.
type CircuitBreakerConfig ¶
type CircuitBreakerConfig struct {
// FailureThreshold is the failure rate (0.0-1.0) that triggers the circuit
// to open (default 0.5 = 50%).
FailureThreshold float64
// MinObservations is the minimum number of requests in the window before
// the failure rate is evaluated (default 5).
MinObservations int
// WindowSize is the number of outcomes tracked in the sliding window (default 20).
WindowSize int
// BaseTimeout is the initial open-state duration (default 30s).
BaseTimeout time.Duration
// MaxTimeout caps the exponential backoff (default 10min).
MaxTimeout time.Duration
// MaxTrips caps the number of consecutive trips before holding at MaxTimeout.
MaxTrips int
}
CircuitBreakerConfig controls the circuit breaker behavior.
func DefaultCircuitBreakerConfig ¶
func DefaultCircuitBreakerConfig() CircuitBreakerConfig
DefaultCircuitBreakerConfig returns sensible defaults.
type CircuitState ¶
type CircuitState int
CircuitState represents the current state of a circuit breaker.
const ( // CircuitClosed is the normal operating state — requests pass through. CircuitClosed CircuitState = iota // CircuitOpen means the circuit has tripped — requests fail immediately. CircuitOpen // CircuitHalfOpen allows one probe request through to test recovery. CircuitHalfOpen )
type DedupOption ¶
type DedupOption func(*dedupMiddleware)
DedupOption configures the dedup middleware.
func WithFingerprintFunc ¶
func WithFingerprintFunc(fn func(job *foxhound.Job) string) DedupOption
WithFingerprintFunc sets a custom function for generating dedup keys. When set, it replaces the default URL-based canonicalization. The function receives the job and returns a string key; identical keys are treated as duplicates.
Example: include HTTP method in the fingerprint to allow GET and POST to the same URL:
middleware.NewDedup(middleware.WithFingerprintFunc(func(job *foxhound.Job) string {
return job.Method + ":" + job.URL
}))
func WithIncludeHeaders ¶
func WithIncludeHeaders(include bool) DedupOption
WithIncludeHeaders includes request headers (from Job.Headers) in the dedup fingerprint. This makes requests with different headers to the same URL be treated as distinct.
func WithKeepFragments ¶
func WithKeepFragments(keep bool) DedupOption
WithKeepFragments preserves URL fragments in the dedup fingerprint. By default, fragments (#section) are stripped before comparison.
type DeltaStore ¶
type DeltaStore interface {
// Seen reports whether key has been marked, and when (zero if not seen).
Seen(key string) (bool, time.Time)
// Mark records that key was fetched now.
Mark(key string) error
// Close releases any resources held by the store.
Close() error
}
DeltaStore persists the set of already-scraped URLs across restarts.
type DeltaStrategy ¶
type DeltaStrategy int
DeltaStrategy controls when DeltaFetch skips a URL.
const ( // DeltaSkipSeen skips any URL that has ever been fetched, regardless of // how long ago. DeltaSkipSeen DeltaStrategy = iota // DeltaSkipRecent skips a URL only if it was fetched within the TTL // window. After the TTL elapses the URL is re-fetched and the timestamp // is refreshed. DeltaSkipRecent )
type DomainDelayConfig ¶
type DomainDelayConfig struct {
// DefaultDelay is the base delay between requests to the same domain.
// Applied to all domains unless overridden.
DefaultDelay time.Duration
// PerDomain overrides the default delay for specific domains.
PerDomain map[string]time.Duration
// Randomize adds log-normal jitter to the delay to appear more human.
Randomize bool
}
DomainDelayConfig configures per-domain download delays.
type MemoryDeltaStore ¶
type MemoryDeltaStore struct {
// contains filtered or unexported fields
}
MemoryDeltaStore is a thread-safe, in-memory DeltaStore. State is lost when the process exits; use a persistent store (e.g. Redis, SQLite) for production cross-run deduplication.
func NewMemoryDeltaStore ¶
func NewMemoryDeltaStore() *MemoryDeltaStore
NewMemoryDeltaStore returns an empty in-memory DeltaStore.
func (*MemoryDeltaStore) Close ¶
func (m *MemoryDeltaStore) Close() error
Close is a no-op for the in-memory store.
func (*MemoryDeltaStore) Mark ¶
func (m *MemoryDeltaStore) Mark(key string) error
Mark implements DeltaStore.
type PersistentCookies ¶
type PersistentCookies struct {
// contains filtered or unexported fields
}
PersistentCookies is a middleware that persists cookies to a JSON file. Cookies are loaded from disk on creation and saved after each request.
func NewPersistentCookies ¶
func NewPersistentCookies(filePath string) (*PersistentCookies, error)
NewPersistentCookies creates a cookie middleware that loads/saves to filePath.
func (*PersistentCookies) FilePath ¶
func (pc *PersistentCookies) FilePath() string
FilePath returns the path where cookies are stored.
func (*PersistentCookies) Load ¶
func (pc *PersistentCookies) Load() error
Load reads cookies from disk into the jar.
func (*PersistentCookies) Save ¶
func (pc *PersistentCookies) Save() error
Save writes the tracked cookies to disk as JSON.
type RedisDeltaStore ¶
type RedisDeltaStore struct {
// contains filtered or unexported fields
}
RedisDeltaStore is a persistent DeltaStore backed by Redis. Keys are stored as "<prefix>:<key>" with the Unix timestamp (seconds) as their value, allowing TTL-aware re-crawl strategies.
RedisDeltaStore is safe for concurrent use.
func NewRedisDeltaStore ¶
func NewRedisDeltaStore(addr, password string, db int, prefix string) (*RedisDeltaStore, error)
NewRedisDeltaStore connects to Redis at addr and returns a RedisDeltaStore.
- addr: host:port, e.g. "localhost:6379"
- password: empty string for no auth
- db: Redis database index (0-15)
- prefix: key namespace, e.g. "foxhound:delta"
func (*RedisDeltaStore) Close ¶
func (r *RedisDeltaStore) Close() error
Close closes the underlying Redis client connection.
func (*RedisDeltaStore) Mark ¶
func (r *RedisDeltaStore) Mark(key string) error
Mark implements DeltaStore. Stores the current Unix timestamp under the namespaced key.
type SQLiteDeltaStore ¶
type SQLiteDeltaStore struct {
// contains filtered or unexported fields
}
SQLiteDeltaStore is a persistent DeltaStore backed by a local SQLite file. State survives process restarts — unlike MemoryDeltaStore — making it suitable for single-node production crawls where Redis is unavailable.
SQLiteDeltaStore is safe for concurrent use; WAL mode is enabled to allow concurrent readers while a single writer commits.
func NewSQLiteDeltaStore ¶
func NewSQLiteDeltaStore(dbPath string) (*SQLiteDeltaStore, error)
NewSQLiteDeltaStore opens (or creates) a SQLite database at dbPath and ensures the deltafetch table exists.
func (*SQLiteDeltaStore) Close ¶
func (s *SQLiteDeltaStore) Close() error
Close closes the underlying SQLite database connection.
func (*SQLiteDeltaStore) Mark ¶
func (s *SQLiteDeltaStore) Mark(key string) error
Mark implements DeltaStore. Inserts or replaces the key with the current Unix timestamp.