Repository navigation
feat(providers): HttpFetcher with retry + feed payload validation (XWING-2223) - #8
Conversation
Accuracy review: retry and payload-validation failure paths need correctionReviewed head: P2 — An empty compressed CSV replaces a good cache and is marked fresh
Through the actual dependent
Please reject empty decoded payloads before cache replacement. The current "corrupt/empty payload cannot overwrite a good cached copy" claim is not true for this case. P2 — Invalid DEFLATE data escapes as
|
Signed-off-by: Kyl-Bempah <24903083+Kyl-Bempah@users.noreply.github.com>
ff70a6a to
cc5e37b
Compare
|
Rebased onto |
- validate_payload checks emptiness after decompression: an empty gzip body is an empty payload, never a cache replacement - gzip.decompress failures normalize to ValidationError (zlib.error joined the caught set; invalid DEFLATE no longer escapes raw) - truncated HTTP bodies (http.client.IncompleteRead) join the retryable transport errors: retried with backoff, ServiceError when exhausted Each reproduced before fixing; regression tests for all five paths.
|
@chubirka all three findings reproduced before fixing, closed in
Regression tests for all five paths (empty gz, invalid DEFLATE, truncated→retry→success, truncated×3→gives up, plus the existing corrupt cases). Gates on this head: lint clean, 891 unit, 200 Postgres integration, coverage 93.9%; ratchet unchanged (3 hits / 2 files). |
Moves the outbound-HTTP part of XWING-2223 out of traust and into core, per the Layers rule from #7: outbound HTTP lives inside providers. This is what traust-security/traust#61 retry code becomes once it moves into core; traust-security/traust#61 shrinks to config + feed metadata and calls this.
Plan note: this PR predates the standardization plans and has no plan file of its own; its home plan is
plans/traust-standardization/4-dependencies/02-kyl-feeds-and-advisory-lookup.md(blocked on Michele's advisory rules). It is the groundwork that plan builds on.What this adds
providers/http.py:HttpFetcher(user_agent, policy, *, clock=, sleep=).get(url) -> bytes, the outbound-HTTP counterpart toProcessRunner.Retry-After(seconds or HTTP-date), capped bymax_wait_seconds.ServiceError; a fail-fast 4xx also sets.status.RetryPolicyis a frozen dataclass:max_attempts,timeout_seconds,backoff_base_seconds,max_wait_seconds,jitter_seconds.Clock(for HTTP-dateRetry-After), so the drift ratchet sees nodatetime.now(). Sleep is injectable for tests.urllibonly, so no new dependency.providers/feeds.py:validate_payload(raw, filename)raisesValidationErrorwhen a download doesn't decode as its file type (gzip / JSON / UTF-8 CSV). This stops a corrupt 200 from overwriting a good cached copy. EPSS, KEV and rh-cve publish no checksum, so "corrupt" means "doesn't decode"; the VEX archive's checksum is XWING-2224.httpandvalidate_payload.Consumer
traust#61 (
feat/feeds-retry): should now just callHttpFetcher&validate_payload. The PR also adds traust-core as a dependency.Fits with #6 (drift ratchet)
Cross-checked on the rebased head: the same 3 baseline hits, nothing new. The
urllib.requestimport inproviders/http.pyis covered by #6'sallowed=("*/providers/*",)forhttp, so the PRs merge in any order without touching.ratchet.json.Quality checklist
ProcessRunneris the pattern)validate_payloadlives with feeds)Changed tests
None — only new tests (
test_http.py,test_feeds.py).Tests
tests/v1/providers/test_http.py: 503→200, timeout→200, 429 withRetry-Afteras seconds, as an HTTP-date and over the cap, backoff doubling and capping, 404 fails fast withstatus, gives up aftermax_attempts. All offline (fakeurlopen, recorded sleeps).tests/v1/providers/test_feeds.py: well-formed vs. empty / corrupt gzip / broken JSON / non-UTF-8 CSV.developmentc061eaa: lint clean, 887 unit passed, ratchet green (cross-checked against feat(checks): drift ratchet tool, baseline, CI gate + docs (XWING-2210) #6's tool + baseline: 3 hits / 2 files, unchanged).Open questions
HttpFetcher, matchingProcessRunner, endorsed — the ticket'sHttpClientdoesn't exist in core and the ratchet's use-instead names the providers, not a client.