Fix SpaceDevs rate-limiting and cache corruption from throttled responses

Don't write rate-limit/error responses to disk in next_launch_api_data,
so they can't become the "most recent" cache file and cause KeyError crashes
in read_cached_launches. Add defensive results-list checks in
read_cached_launches and get_launches to handle any existing bad files.

Drop refresh=True from the updater's get_active_crewed_flights call so the
2-hour TTL applies; the paginated spacecraft/flight crawl was running on
every hourly cron job and likely causing the burst that triggered throttling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-02-21 20:50:05 +00:00
parent b4f0a5bf5d
commit 9f881d7177
2 changed files with 24 additions and 11 deletions

View file

@ -31,6 +31,9 @@ def next_launch_api_data(rocket_dir: str, limit: int = LIMIT) -> StrDict | None:
data: StrDict = r.json() data: StrDict = r.json()
except requests.exceptions.JSONDecodeError: except requests.exceptions.JSONDecodeError:
return None return None
# Only persist valid launch payloads; rate-limit / error responses must not
# overwrite the cache or they become the "most recent" file.
if isinstance(data.get("results"), list):
open(filename, "w").write(r.text) open(filename, "w").write(r.text)
return data return data
@ -328,7 +331,9 @@ def load_cached_launches(rocket_dir: str) -> StrDict | None:
def read_cached_launches(rocket_dir: str) -> list[Summary]: def read_cached_launches(rocket_dir: str) -> list[Summary]:
"""Read cached launches.""" """Read cached launches."""
data = load_cached_launches(rocket_dir) data = load_cached_launches(rocket_dir)
return [summarize_launch(launch) for launch in data["results"]] if data else [] if not data or not isinstance(data.get("results"), list):
return []
return [summarize_launch(launch) for launch in data["results"]]
def get_launches( def get_launches(
@ -361,10 +366,20 @@ def get_launches(
except Exception: except Exception:
pass # fallback to cached version pass # fallback to cached version
f = existing[0][1] # Find the most recent cache file that contains a valid results list.
# Older files without "results" (e.g. stale rate-limit responses) are skipped.
data = None
for _, f in existing:
filename = os.path.join(rocket_dir, f) filename = os.path.join(rocket_dir, f)
data = json.load(open(filename)) try:
candidate = json.load(open(filename))
except (json.JSONDecodeError, OSError):
continue
if isinstance(candidate.get("results"), list):
data = candidate
break
if not data:
return []
upcoming = [summarize_launch(launch) for launch in data["results"]] upcoming = [summarize_launch(launch) for launch in data["results"]]
active_crewed = get_active_crewed_flights(rocket_dir, refresh=refresh) or [] active_crewed = get_active_crewed_flights(rocket_dir, refresh=refresh) or []
by_slug = { by_slug = {

View file

@ -321,11 +321,9 @@ def update_thespacedevs(config: flask.config.Config) -> None:
existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir) existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir)
assert existing_data assert existing_data
# Refresh active crewed mission cache used by the launches page. # Update active crewed mission cache used by the launches page.
# Failures are handled internally with cache fallback. # Uses the 2-hour TTL; failures are handled internally with cache fallback.
active_crewed = agenda.thespacedevs.get_active_crewed_flights( active_crewed = agenda.thespacedevs.get_active_crewed_flights(rocket_dir)
rocket_dir, refresh=True
)
# Always follow configured slugs # Always follow configured slugs
follow_slugs: set[str] = set(config["FOLLOW_LAUNCHES"]) follow_slugs: set[str] = set(config["FOLLOW_LAUNCHES"])