Components
This page maps every real module under src/ to what it does. It is a companion to overview.md, which covers the request flow, the redirect-based delivery trade-off, and the two required diagrams. Every module and file path below was checked against the code on main, not assumed from names.
Entry point
src/main.rs
Builds the Tokio runtime by hand (not #[tokio::main]) so worker_threads can be a runtime value read from TOKIO_WORKER_THREADS, cgroup-aware via modules::utils::cgroup::effective_cpu_count rather than num_cpus::get() directly (a cgroup-limited container otherwise oversizes the runtime for the host's full core count). Sets mimalloc::MiMalloc as the global allocator. Installs a SIGTERM/SIGINT-triggered graceful shutdown with a configurable drain deadline (SHUTDOWN_TIMEOUT_SECS, default 20s — comfortably under Kubernetes' default 30s terminationGracePeriodSeconds).
src/bin/healthcheck.rs, src/bin/benchmark.rs
Two extra binaries: a standalone healthcheck probe, and a load-generating benchmark client (serves fixture images locally and drives the resize endpoint against them — see .bench-baseline/BASELINE.md for how its output feeds the performance numbers cited in overview.md).
Router and middleware — src/modules/router/
router.rs— hand-written route table (build_app):GET /health(unauthenticated, Kubernetes probe target),GET /api/images/files/{key}(unsigned download route),GET /{signature}/{*rest}(the imgproxy-compatible signed resize route, mounted at the root — axum/matchit prefers a literal path segment likeapi/healthover this route's dynamic first segment, so it never shadows the others), and, only when built with--features otel,GET /metricsbehindmetrics_auth::require_metrics_auth. There is nogen_server-generated router anywhere in this file or this codebase — see "Removed" below.middlewares.rs—MiddlewareConfig(env-driven:REQUEST_TIMEOUT_SECS,MAX_CONCURRENT_REQUESTS,RATE_LIMIT_BURST,RATE_LIMIT_PERIOD_MS) plusapply_common_middlewares, which layers, outer to inner: CORS,tower_governor's per-IP token-bucketGovernorLayer(429 on excess — its per-key state is pruned every 60s vialimiter.retain_recent()so it doesn't grow unbounded over the process lifetime), a conditional- GET middleware for the download route (ETag/If-None-Match/304, computed directly from the requested key without touching storage), aSemaphore-backed saturation/timeout guard (503 on either), andtower-http'sCompressionLayer(br/deflate/gzip/zstd) closest to the app.
API handlers — src/modules/api/
handler.rs—ApiService, the shared application state:resize_service,signing,presets/allowed_options(issue #52), and (underotel)metrics_auth.ApiService::createbuilds every sub-config fromEnvConfigand fails closed at startup for signing,/metricsauth, and preset parsing — a misconfiguration is a boot-time error, not a per-request surprise.resize.rs—GET /{signature}/{processing_options}/{plain|base64 source}.{extension}. Splits the path (modules::url::split), verifies the signature, parses options+source (SignedRequest::parse_with_config), resolves.autoviamodules::negotiation::resolve, callsResizeService::resize, and builds an explicit301(not axum'sRedirect::permanent, which issues308) withCache-Control: public, max-age=31536000, immutableand, only for a negotiated.autorequest,Vary: Accept.download.rs—GET /api/images/files/{key}, deliberately unsigned (this route only ever serves bytes the resize route already produced and cached under a content-derived key; the strict key grammar inservices::storage::key_validationis the guard here, not a signature). DerivesContent-Typefrom the key's own extension.
URL grammar and signing
src/modules/url/
Implements the imgproxy-compatible signed-URL grammar /{signature}/{processing_options}/{plain|base64 source}.{extension}:
mod.rs—split(cheap extraction of the signature segment and the exact signed byte string, run before any full parse so an unauthenticated caller can't use parse-error content as an oracle) andSignedRequest::parse/parse_with_config(full options+source grammar, presets and the processing-option allowlist applied ahead of the plain grammar parse).options.rs—ProcessingOptions, parsing/-delimitedcode:arg1:arg2segments (rs,q,bl,g,el,bg,ar,sm(strip-metadata, defaulttrue— issue #5),fq,webpo,jpgo/mb(JPEG progressive/chroma-subsampling tuning and max-bytes search — issue #76), crop/gravity, rotate/flip/trim/extend/padding/zoom/dpr/min-width/min-height, watermark, preset references).presets.rs—PresetRegistry(parses thePRESETSenv var: named, reusable option-segment lists, imgproxy-compatible; a preset nameddefaultis auto-prepended to every request) andAllowedOptions(parsesALLOWED_PROCESSING_OPTIONS, an allowlist of which option codes a deployment permits at the top level of a URL).source.rs—SourceSpec/parse_source: eitherplain/{percent-encoded URL}.{extension}or a single base64url-encoded segment, with the trailing.{extension}mandatory and mapped toImageFormat(including the.autonegotiation trigger).
src/modules/signing/
verify.rs—compute_signature/verify_signature/sign:HMAC-SHA256(key, salt || signed_path), base64url (no padding), constant-time comparison via thesubtlecrate.config.rs—SigningConfig::from_env, which fails closed at startup (issue #27): a deployment must either configure a real key/salt or explicitly setALLOW_UNSIGNED_REQUESTS=trueto use the/unsigned/...escape hatch.
src/modules/negotiation.rs
resolve(format, accept) — resolves ImageFormat::Auto against the request's Accept header, preferring AVIF, then WebP, falling back to JPEG, weighted by each entry's q parameter. Any non-Auto format passes through unchanged with negotiated = false.
src/modules/metrics_auth/
Bearer-token authentication for GET /metrics (issue #77), gated behind the otel feature since that's the only build where /metrics is ever mounted at all. MetricsAuthConfig::from_env fails closed at startup, mirroring SigningConfig.
Services — src/services/
resize/handler.rs — ResizeService
The orchestrator tying cache, image processing, and storage together:
resize(params)— generates the cache key, checks the cache, and on a miss runs single-flight coalescing before callingdo_resize_work. Seeoverview.md's "Concurrency model" and its cache-miss sequence diagram for the full mechanics ofInFlightMap/InFlightGuard(issue #37).do_resize_work— the actual download → process → upload pipeline for a confirmed miss.download(params)— serves the unsigned/api/images/files/{key}route by reading directly from storage; a missing/expired key surfaces as a "not found" message thatAppError::classify_download_errormaps to404.resize_batch— bounded-concurrency batch resizing overfutures::stream::buffer_unordered.
image/handler.rs — ImageService
Owns the two semaphores (download_semaphore, processing_semaphore), the SSRF-guarded fetch path, and the decode/resize/encode pipeline:
fetch_validated/download_image— seeoverview.md's "Security boundary" section for the SSRF guard itself (implemented insource_guard.rs, described next).download_imagealso enforces the streaming size cap.process_image— acquiresprocessing_semaphore(non-blockingtry_acquire_owned,503on exhaustion) and runs decode/resize/encode onspawn_blocking, with atx.is_closed()check to skip queued-but-not-yet-started work when the caller has already disconnected.process_image_blocking_with_limits*— header-only dimension peek and resolution check (decompression-bomb guard) before any full decode; EXIF autorotate, trim, and explicit crop are applied in that order (matching imgproxy's own pipeline ordering) before the actual resize; animated GIF/WebP sources are detected and routed toencode_animationseparately.decode_with_limits— dispatches by source format, each with a fallback to the plainimage-crate decoder (decode_with_image_crate) on failure:- JPEG —
decode_jpeg_scaled(mozjpeg/libjpeg-turbo, DCT-scaled when a resize makes a smaller decode safe, full-size otherwise; issue #67). - WebP —
decode_webp_libwebp(real libwebp via FFI, issue #66) — replaces theimagecrate's pure-Rustimage-webpdecoder. - AVIF —
avif_codec::decode(libavif/dav1d, issue #67/#68) — the first release where AVIF is accepted as a source format at all; previously any.avifsource was rejected outright. - PNG/GIF — always
decode_with_image_crate(theimagecrate's own decoders); untouched by the above. encode_with_max_bytes— binary-searches JPEG quality down until encoded output fits a requestedmax_bytesbudget (issue #76), bounded to a fixed number of extra encode attempts.
image/avif_codec.rs
AVIF encode and decode via libavif, both directions (issue #67/#68): encode (AOM backend, DEFAULT_AVIF_SPEED = 6) and decode/peek_dimensions (dav1d backend, the latter re-running the same megapixel-overflow-checked resolution guard ImageService::check_source_resolution does, as defense in depth around libavif's own header parse). Replaces the pure-Rust ravif/rav1e encoder and adds AVIF source decode, which this service previously had no path for at all.
image/source_guard.rs
Pure, deterministic SSRF validation logic (the one exception is resolve_validated_addr, which performs the actual DNS lookup): validate_scheme, is_allowed_source/matches_allowed_prefix, is_blocked_ip_with_policy (and its IPv4/IPv6 halves), and IPv4-literal decoding covering decimal/octal/hex smuggling forms. See overview.md's "Security boundary" for the full threat model this covers and why redirect revalidation and DNS pinning are both necessary.
cache/handler.rs — CacheService
generate_key(params) — the SHA-256, length-prefixed, versioned cache key described in overview.md's "Cache key design". CACHE_KEY_VERSION is currently 11; its doc comment on this file is the authoritative history of what each version bump added — including v10 (metadata-strip default, issue #5), v11 (the AVIF encoder cutover to libavif/AOM) — and, in one case (issue #67's WebP decoder swap to libwebp), why a bump was deliberately not taken.
storage/
core.rs— theStorageBackendtrait:upload_image/upload_image_with_ttl,check_cache,get_image,delete.ttl: Nonemeans "never expires."handler.rs—StorageService/StorageConfig/StorageType, selecting a backend by Cargo feature and (when more than one is compiled in)STORAGE_TYPE. Also enforces that a configuredkey_prefix(fromSTORAGE_SUB_PATH) matches whatCacheService::generate_keyactually produces.s3_handler.rs(features3) — S3/MinIO-compatible backend viaaws-sdk-s3.local_fs_handler.rs(featurelocal_fs) — local-directory backend.in_memory_handler.rs(featurein_memory, test-only) — unboundedHashMap-backed backend,#[cfg(all(test, feature = "in_memory"))]— does not exist in a release build at all, regardless of the Cargo feature flag; selecting it viaSTORAGE_TYPEoutside tests fails fast at startup instead of running an uncapped cache in production.key_validation.rs—validate_cache_key, the strict grammar (<prefix><64 lowercase hex>.<jpg|png|webp|avif|gif>) every backend is checked against before a key ever reaches it, closing path-traversal and S3 IDOR in one place rather than per backend.
health/handler.rs
health() — returns the literal string "OK". Deliberately unauthenticated; see overview.md's "Security boundary" for why.
metrics/handler.rs
metrics_handler() — encodes the global Prometheus registry (prometheus::gather()) as text. Only compiled/mounted under the otel feature; protected by modules::metrics_auth at the router layer, not in this handler itself.
Models — src/models/params.rs
ResizeQuery (the fully-parsed representation of a resize request — url, width/height, ResizeType, ImageFormat, quality/format-quality overrides, crop/gravity, geometry operations, watermark, etc.) and DownloadPathParams ({ key: String }, hand-written — see "Removed" below).
Configuration
src/modules/env/env.rs—EnvConfig, theenvconfig-derived top-level environment binding (HTTP host/port, storage credentials,MAX_IMAGE_SIZE_MB,MAX_SRC_RESOLUTION_MP,ALLOWED_SOURCES,ALLOW_LOOPBACK_SOURCE_ADDRESSES/ALLOW_LINK_LOCAL_SOURCE_ADDRESSES,max_redirects, presets/allowlist strings, etc.).src/config/performance.rs—PerformanceConfig, derived fromEnvConfigplus a small set of named default profiles (default/high-throughput/low-latency/memory-constrained), each settingmax_image_size,max_src_resolution_mp,max_redirects, download/processing concurrency limits, and the SSRF-guard override flags.
Utility modules — src/modules/utils/
date.rs— a minimal hand-rolled RFC 7231IMF-fixdateformatter (no date/time crate is a dependency of this crate), used for the download route'sLast-Modifiedheader.etag.rs—if_none_match_satisfied, RFC 7232 §3.2 strong-comparison matching against the single server-computedETagthe conditional-download middleware produces.err.rs—AppErrorand itsclassify_download_error/classify_resize_errorconstructors, mapping internal failures to HTTP status codes (seeoverview.md's rejection-state diagram for the concrete mapping). Every variant renders withCache-Control: no-store.cgroup.rs—effective_cpu_count, reading cgroup v2cpu.maxor cgroup v1cpu.cfs_quota_us/cpu.cfs_period_usdirectly (falling back tonum_cpus::get()), used bymain.rsto size the Tokio runtime.
Tracing — src/modules/tracer/
OpenTelemetry tracing/metrics initialization (init.rs), compiled only under the otel feature.
Removed since the last documentation pass
The following existed in an earlier version of this service and no longer do. They are recorded here only as history, not as current architecture:
- The generated OpenAPI server (
gen-server, apackages/directory,openapi.yaml). Issue #53 replaced it with the hand-written router insrc/modules/router/router.rsand the hand-written handlers insrc/modules/api/. Verified:grep -rc 'gen-server\|gen_server\|openapi' Cargo.tomlandfind . -iname 'openapi.yaml'both return nothing in this tree. rayon— the old CPU-bound processing pool. Replaced bytokio::task::spawn_blockingbounded byprocessing_semaphore; seeimage/handler.rs's doc comment onprocess_imagefor why rayon's intra-job work-stealing was never actually used. Not a dependency inCargo.toml.o2o,lru,axum-extra— none appear inCargo.toml's dependency list; no code undersrc/references them.