refactor: extract httpfetcher package from imgcache
All checks were successful
check / check (push) Successful in 57s

Move HTTPFetcher, Config (was FetcherConfig), SSRF-safe dialer, rate
limiting, content-type validation, and related error vars from
internal/imgcache/fetcher.go into new internal/httpfetcher/ package.

The Fetcher interface and FetchResult type also move to httpfetcher
to avoid circular imports (imgcache imports httpfetcher, not the other
way around).

Renames to avoid stuttering:
  NewHTTPFetcher -> httpfetcher.New
  FetcherConfig  -> httpfetcher.Config
  NewMockFetcher -> httpfetcher.NewMock

The ServiceConfig.FetcherConfig field is retained (it describes what
kind of config it holds, not a stutter).

Pure refactor - no behavior changes. Unit tests for the httpfetcher
package are included.

refs #39
This commit is contained in:
clawbot
2026-04-17 06:47:05 +00:00
parent 6b4a1d7607
commit a853fe7ee7
12 changed files with 414 additions and 74 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/dustin/go-humanize"
"sneak.berlin/go/pixa/internal/allowlist"
"sneak.berlin/go/pixa/internal/httpfetcher"
"sneak.berlin/go/pixa/internal/imageprocessor"
"sneak.berlin/go/pixa/internal/magic"
)
@@ -19,7 +20,7 @@ import (
// Service implements the ImageCache interface, orchestrating cache, fetcher, and processor.
type Service struct {
cache *Cache
fetcher Fetcher
fetcher httpfetcher.Fetcher
processor *imageprocessor.ImageProcessor
signer *Signer
allowlist *allowlist.HostAllowList
@@ -33,9 +34,9 @@ type ServiceConfig struct {
// Cache is the cache instance
Cache *Cache
// FetcherConfig configures the upstream fetcher (ignored if Fetcher is set)
FetcherConfig *FetcherConfig
FetcherConfig *httpfetcher.Config
// Fetcher is an optional custom fetcher (for testing)
Fetcher Fetcher
Fetcher httpfetcher.Fetcher
// SigningKey is the HMAC signing key (empty disables signing)
SigningKey string
// Whitelist is the list of hosts that don't require signatures
@@ -57,15 +58,15 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
// Resolve fetcher config for defaults
fetcherCfg := cfg.FetcherConfig
if fetcherCfg == nil {
fetcherCfg = DefaultFetcherConfig()
fetcherCfg = httpfetcher.DefaultConfig()
}
// Use custom fetcher if provided, otherwise create HTTP fetcher
var fetcher Fetcher
var fetcher httpfetcher.Fetcher
if cfg.Fetcher != nil {
fetcher = cfg.Fetcher
} else {
fetcher = NewHTTPFetcher(fetcherCfg)
fetcher = httpfetcher.New(fetcherCfg)
}
signer := NewSigner(cfg.SigningKey)
@@ -113,7 +114,7 @@ func (s *Service) Get(ctx context.Context, req *ImageRequest) (*ImageResponse, e
"path", req.SourcePath,
)
return nil, fmt.Errorf("%w: %w", ErrUpstreamError, ErrNegativeCached)
return nil, fmt.Errorf("%w: %w", httpfetcher.ErrUpstreamError, ErrNegativeCached)
}
// Check variant cache first (disk only, no DB)
@@ -418,13 +419,13 @@ const (
// isNegativeCacheable returns true if the error should be cached.
func isNegativeCacheable(err error) bool {
return errors.Is(err, ErrUpstreamError)
return errors.Is(err, httpfetcher.ErrUpstreamError)
}
// extractStatusCode extracts HTTP status code from error message.
func extractStatusCode(err error) int {
// Default to 502 Bad Gateway for upstream errors
if errors.Is(err, ErrUpstreamError) {
if errors.Is(err, httpfetcher.ErrUpstreamError) {
return httpStatusBadGateway
}