rss-cli 0.2.0

An AI-friendly, cache-backed RSS / Atom / JSON Feed CLI that also runs as an MCP server
Documentation
# 5. Conditional GET with an always-revalidate default

- **Status:** Accepted
- **Date:** 2026-06-01

## Context

Given a cache ([ADR-0004](0004-file-based-atomic-cache.md)), we must choose a default
freshness policy. A TTL ("serve from cache for N minutes without checking the network")
is fast but **unpredictable for an agent**: whether you get fresh data depends on the
invisible age of a local file. For a tool whose value is predictability, surprising
staleness is worse than a cheap network round-trip.

## Decision

The default policy is **always revalidate** via a conditional GET. Implemented in
[`src/fetch.rs`](../../src/fetch.rs) as the `CachePolicy` enum:

- **`Revalidate` (default):** every fetch sends `If-None-Match` (from the cached `ETag`)
  and `If-Modified-Since` (from the cached `Last-Modified`). A `304 Not Modified` serves
  the cached body with `status: "not_modified"`, `from_cache: true`, and refreshes only
  `fetched_at`. A `200` stores the new body + validators and returns fresh content.
- **`MaxAge(d)` (opt-in, `--max-age`):** if a cache entry is younger than `d`, return it
  with **no network call at all**; otherwise fall through to revalidate. This is the
  explicit "trade freshness for speed" knob.
- **`NoCache` (`--no-cache`):** a plain GET that neither reads nor writes the cache.
- `--refresh` forces revalidation even when `--max-age` would otherwise serve from cache.

So back-to-back default calls *do* touch the network — but with a single cheap conditional
request that usually returns an empty `304`.

## Consequences

- **Predictable by default:** an agent gets current data every call, while still saving
  bandwidth (the body only transfers on real change) and being polite to origin servers.
- The speed-over-freshness path (`--max-age`) exists but is opt-in and obvious in the
  output (`from_cache: true`, `not_modified: true`), so staleness is never silent.
- A server that returns `304` with no cache entry present is treated as an error (it
  should never happen) rather than silently returning an empty body.
- `status: "not_modified"` is surfaced as a first-class outcome in the output contract
  ([ADR-0006]0006-ai-facing-output-contract.md) so callers can cheaply detect "nothing
  changed."

## Alternatives considered

- **TTL-by-default (serve from cache for N minutes).** Rejected as the default for
  unpredictable staleness; kept as the opt-in `--max-age` behavior.
- **Honoring `Cache-Control: max-age` from the response.** Out of scope for v1; the
  conditional-GET default already covers the common case, and an explicit `--max-age`
  gives the caller direct control without parsing/obeying origin cache directives.