Skip to content

fix(photos): stop caching a failed lookup as a confirmed absence - #67

Merged
ifnull merged 1 commit into
mainfrom
fix/photo-failure-cache
Aug 31, 2026
Merged

ifnull merged 1 commit into
mainfrom
fix/photo-failure-cache

Conversation

@ifnull

@ifnull ifnull commented Aug 31, 2026

Copy link
Copy Markdown
Owner

What you saw

{"hex": "aa7e36", "error": "Server error '525 <none>' for url '...planespotters.net...'", "event": "photo_lookup_failed"}
{"hex": "aa9e4a", "error": "", "event": "photo_lookup_failed"}
{"hex": "a83f98", "error": "Server error '525 <none>' for url '...planespotters.net...'", "event": "photo_lookup_failed"}

The 525s are upstream — Cloudflare failing its SSL handshake to Planespotters'
origin. Nothing to fix on our side. But the log surfaced two real bugs.

Bug 1: a blip blanks the photo for a month

photo_for() cached whatever _fetch() returned under cache_ttl_days, and
_fetch() returned None for two very different things:

  • upstream answering "no photo for this hex" — good for weeks
  • upstream never answering — says nothing about the airframe

Same None, same 30-day TTL. So those three hexes would have had no photo
until late September, for an outage that lasted minutes.

Failures now carry their own 5-minute TTL. Short enough that a blip costs one
alert's photo rather than a month's; long enough that a sustained outage costs
one request per aircraft per five minutes instead of one per alert.

The cache entry carries its own ttl rather than the reader branching on
result is None, so a confirmed absence and a failure can never share an
expiry by accident. _fetch returns (result, answered) to make the
distinction explicit at the only place that can actually tell them apart.

Bug 2: "error": ""

That second log line reports a failure and says nothing about it. httpx's
timeout, connect, and protocol errors all stringify to "" when raised without
a message:

ReadTimeout            str() -> ''
ConnectTimeout         str() -> ''
ConnectError           str() -> ''
RemoteProtocolError    str() -> ''

Now logs error_class and retry_in_s, matching the shape
receiver_fetch_failed already uses.

Tests

Five new, covering: failure retries after the short TTL; a failure does not get
the success TTL; a confirmed absence still keeps the long one (the regression
risk in fixing this); a 200 with no usable thumbnail counts as answered; and the
empty-str() logging case.

813 pass, ruff + mypy clean.

🤖 Generated with Claude Code

photo_for() cached whatever _fetch() returned under cache_ttl_days, and _fetch()
returned None for two very different things: upstream answering "no photo for
this hex", and upstream never answering at all. The first is good for weeks.
The second says nothing about the airframe.

So a single transient error blanked that aircraft's photo for the full success
TTL — 30 days by default. Observed in the field: Cloudflare returned 525 (SSL
handshake failed to origin) for api.planespotters.net on three hexes within
twenty minutes. Those aircraft would have had no photo until late September,
for an outage that lasted minutes.

Failures now carry their own five-minute TTL. Short enough that a blip costs
one alert's photo rather than a month's, long enough that a sustained outage
costs one request per aircraft per five minutes instead of one per alert. The
cache entry carries its own ttl rather than the reader branching on result-is-
None, so a confirmed absence and a failure can never share an expiry by
accident. _fetch returns (result, answered) to make the distinction explicit at
the only place that can actually tell them apart.

Also logs error_class and retry_in_s on the failure path. httpx's timeout,
connect, and protocol errors all stringify to "" when raised without a message,
which produced log lines reading {"hex": "aa9e4a", "error": ""} — a failure
report carrying no indication of what failed. Same field shape
receiver_fetch_failed already uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiVqZDdasMeshTd7CqdxEp
@ifnull
ifnull merged commit 717a491 into main Aug 31, 2026
3 checks passed
@ifnull
ifnull deleted the fix/photo-failure-cache branch August 31, 2026 03:56
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.

1 participant