middleware

package
v0.0.25 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package middleware provides composable foxhound.Middleware implementations for rate limiting, deduplication, depth limiting, and retry logic.

Index

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.

func (*MemoryDeltaStore) Seen

func (m *MemoryDeltaStore) Seen(key string) (bool, time.Time)

Seen 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.

func (*PersistentCookies) Wrap

Wrap implements foxhound.Middleware.

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.

func (*RedisDeltaStore) Seen

func (r *RedisDeltaStore) Seen(key string) (bool, time.Time)

Seen implements DeltaStore. Returns (true, timestamp) if the key exists, otherwise (false, zero time).

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.

func (*SQLiteDeltaStore) Seen

func (s *SQLiteDeltaStore) Seen(key string) (bool, time.Time)

Seen implements DeltaStore. Returns (true, time) when key is found, (false, zero) otherwise.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL