Skip to content

Respect ESI Expires header to skip HTTP requests within TTL window#10

Merged
ronaldvdmeer merged 5 commits intomainfrom
feat/expires-ttl-caching
Mar 29, 2026
Merged

Respect ESI Expires header to skip HTTP requests within TTL window#10
ronaldvdmeer merged 5 commits intomainfrom
feat/expires-ttl-caching

Conversation

@ronaldvdmeer
Copy link
Copy Markdown
Owner

Summary

The client now respects the Expires header returned by ESI, skipping HTTP requests entirely when cached data is still fresh. This is the most quota-efficient caching layer: no network traffic is generated until the server-side cache window closes.

Details

New caching layers (two-tier)

Layer Trigger Benefit
TTL (Expires) Within ESI cache window Zero HTTP requests
ETag (If-None-Match / 304) After TTL expires No response body downloaded

Implementation

  • Expires header is parsed via email.utils.parsedate_to_datetime and stored as expires_at: datetime | None alongside the ETag (cache tuple extended from 3 to 4 elements)
  • New _get_fresh_cached(method, cache_key) helper: returns (data, x_pages) if the cache entry exists and has not expired, otherwise None
  • _request_full() short-circuits immediately when _get_fresh_cached returns a result, before any HTTP call is initiated
  • New _finalize_response(method, cache_key, response) helper extracts JSON, parses the X-Pages header, and stores the ETag/Expires entry — cleans up _request_full internals
  • Malformed or absent Expires headers store None; the ETag flow proceeds as before

Documentation

  • docs/error-handling.md — updated "ESI caching headers" section to document both the TTL and ETag caching layers and removed the outdated note claiming the library does not cache responses

Testing

  • TestExpiresTTLCaching — 5 new tests:
    • test_expires_stored_in_cache — valid Expires header is parsed and stored
    • test_fresh_cache_skips_http_request — no HTTP call when TTL is still valid
    • test_expired_cache_sends_etag_request — ETag flow triggered after TTL expires
    • test_malformed_expires_stores_none — bad header stored as None
    • test_no_expires_header_stores_none — absent header stored as None

Result: 172 tests passed, 98.40% coverage, pylint 10.00/10, ruff + mypy clean

- Parse the Expires header on successful GET responses and store it
  as expires_at (datetime | None) alongside the ETag in the cache
  (cache tuple extended from 3 to 4 elements)
- Add _get_fresh_cached() to check whether a cache entry is still
  within its TTL; _request_full() short-circuits immediately if so,
  avoiding any HTTP request and protecting the ESI error quota
- Add _finalize_response() to encapsulate JSON parsing, x_pages
  extraction and ETag/Expires storage after a successful response
- Malformed or absent Expires headers are handled gracefully (None
  stored; falls through to the existing ETag/If-None-Match flow)
- Add TestExpiresTTLCaching with 5 tests covering storage, TTL hit,
  TTL miss, malformed header, and absent header
- Update docs/error-handling.md to document both caching layers
Copilot AI review requested due to automatic review settings March 29, 2026 18:35

This comment was marked as resolved.

- Normalize naive Expires datetimes to UTC instead of storing them
  as-is (would cause TypeError when comparing with datetime.now(UTC))
- Extract _parse_expires() static method to avoid duplicated parsing
  logic between _store_etag and the 304 branch
- Update 304 branch to refresh the Expires timestamp from the response
  headers so re-validated entries re-enter the TTL window
- Update _request_full docstring to describe both TTL and ETag layers
- Fix test_fresh_cache_skips_http_request: keep both calls inside an
  aioresponses() context so a regression never hits the real network
- Update docs/error-handling.md: accurate wording that caching only
  applies when a cacheable GET response with ETag was received
Copilot AI review requested due to automatic review settings March 29, 2026 18:43

This comment was marked as resolved.

- Fix _parse_expires docstring: timezone-aware not UTC-aware (only
  naive values are patched to UTC; other timezones are kept as-is)
- Fix _finalize_response docstring: applies to any 2xx response,
  not only GET (caching is still GET-only via _store_etag)
- Add If-None-Match assertion to test_expired_cache_sends_etag_request
  to verify the ETag header is actually sent after TTL expiry
@ronaldvdmeer ronaldvdmeer merged commit c30a0d8 into main Mar 29, 2026
6 checks passed
@ronaldvdmeer ronaldvdmeer deleted the feat/expires-ttl-caching branch March 29, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants