fix: stop treating HTTP 304 as a feed error (#9)#10
Merged
Conversation
A 304 Not Modified is the expected answer to a conditional request whose
validators are still current, but the fetcher turned it into
Err(FeedParse("304 Not Modified")) and identified it downstream by string
matching. crawl_feed never did that match, so a healthy 304 fell into its
generic error handler and set status = "error".
The scheduler then only listed feeds with status = "active", making "error"
terminal: the feed was never crawled again until a restart, where config sync
resets the status.
Together, any feed whose server honours ETags was bricked on the first crawl
after a successful one.
- fetch_with_conditions now returns FetchOutcome::{Fetched, NotModified}, so
the compiler forces every caller to handle 304 rather than relying on a
string sentinel that one caller can forget.
- On 304, mark_feed_unchanged records a healthy poll and clears stale error
state while preserving etag/last_modified - clearing them would make the
next request unconditional.
- The scheduler polls via list_crawlable_feeds, which retries feeds in "error"
(failures are usually transient) but still skips deliberately "disabled" ones.
- Removed the dead fetch_feed/FetchedFeed pair: it was the only code that
handled 304 correctly, had zero callers, and made the bug look handled.
- Send If-Modified-Since as an HTTP-date. crawl_feed sent RFC 3339, which is
not an HTTP-date at all, and the recipe path used to_rfc2822, which renders
the zone as "+0000" rather than "GMT", so servers ignored the header.
Existing feeds stuck in "error" recover on the next scheduler tick; no
migration needed.
ReviewSolid fix — good root-cause diagnosis (dead Strengths
Things worth a look (non-blocking)
Test coverageGood balance of true integration tests (mockito HTTP semantics) and DB-level tests (retry-inclusion, unchanged-status bookkeeping) targeting exactly the two bugs from #9. The explicit "500 still errors" test is a nice touch — it guards against a fix that's too aggressive as well as one that's too narrow. Nothing here blocks merging — the flagged items are forward-looking, not defects in this diff. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9.
What was broken
Two bugs compounded into a feed that dies permanently and silently.
A 304 was treated as a fetch failure.
fetcher.rsturned HTTP 304 intoErr(Error::FeedParse("304 Not Modified"))— smuggling a success through the error type, identified downstream by string matching.crawl_feednever did that string match, so a healthy 304 fell into its generic error handler and setstatus = "error".Errored feeds were never retried. The scheduler only listed feeds with
status = "active", making"error"a terminal state. Only a process restart unstuck a feed, because config sync resets the status on boot.Together: every feed whose server correctly honours ETags gets bricked on the first crawl after a successful one. That is exactly the log in #9 — 13:38 success (stores ETag) → 14:38 conditional request → 304 → error → silence.
The smoking gun:
Crawler::fetch_feedwas the only function that handled 304 correctly, and it had zero callers. Dead code. The live path throughcrawl_feedhad simply forgotten the case, and the dead duplicate made it look handled.The fix
fetch_with_conditionsnow returnsFetchOutcome::{Fetched, NotModified}instead ofResult<FetchResult>, so the compiler forces every caller to confront the 304 case. Adding a third string-match would have repeated the exact fragility that caused this.mark_feed_unchanged, which clears stale error state but deliberately preservesetag/last_modified— clearing them would make the next request unconditional.list_crawlable_feeds, which includeserrorfeeds so transient failures self-heal, while still excluding deliberatelydisabledones.fetch_feed/FetchedFeedpair.One adjacent bug fixed because it lives in the same call:
crawl_feedsentIf-Modified-Sinceas RFC 3339, which is not a valid HTTP-date at all, and the recipe path used chrono'sto_rfc2822, which renders the zone as+0000instead ofGMT. Servers were ignoring the header, so feeds without an ETag re-downloaded every hour. Both now use a proper IMF-fixdate helper.Tests
Four mockito integration tests in
tests/conditional_request_test.rs: 304-via-ETag, 304-via-Last-Modified, a normal 200 with caching headers, and confirmation that a 500 still errors so genuinely broken feeds are still flagged. Plus DB tests pinning the retry-errored-but-skip-disabled selection and the 304 bookkeeping.SSRF validation blocks loopback URLs (
validation.rs:70), socrawl_feedcannot be driven end-to-end against a mock server — I tested the two real seams instead rather than weaken that check for tests.Full suite passes (80 tests), clippy and rustfmt clean.
Deployment
No migration needed. Feeds currently stuck in
errorare picked up automatically on the next scheduler tick.