From 9f881d71776d09ff1622f0d33eb205f63ab82e20 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 21 Feb 2026 20:50:05 +0000 Subject: [PATCH] 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 --- agenda/thespacedevs.py | 27 +++++++++++++++++++++------ update.py | 8 +++----- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index e4f1b7a..5b5cbf9 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -31,7 +31,10 @@ def next_launch_api_data(rocket_dir: str, limit: int = LIMIT) -> StrDict | None: data: StrDict = r.json() except requests.exceptions.JSONDecodeError: return None - open(filename, "w").write(r.text) + # 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) return data @@ -328,7 +331,9 @@ def load_cached_launches(rocket_dir: str) -> StrDict | None: def read_cached_launches(rocket_dir: str) -> list[Summary]: """Read cached launches.""" 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( @@ -361,10 +366,20 @@ def get_launches( except Exception: pass # fallback to cached version - f = existing[0][1] - - filename = os.path.join(rocket_dir, f) - data = json.load(open(filename)) + # 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) + 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"]] active_crewed = get_active_crewed_flights(rocket_dir, refresh=refresh) or [] by_slug = { diff --git a/update.py b/update.py index a059e63..97e4d41 100755 --- a/update.py +++ b/update.py @@ -321,11 +321,9 @@ def update_thespacedevs(config: flask.config.Config) -> None: existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir) assert existing_data - # Refresh active crewed mission cache used by the launches page. - # Failures are handled internally with cache fallback. - active_crewed = agenda.thespacedevs.get_active_crewed_flights( - rocket_dir, refresh=True - ) + # Update active crewed mission cache used by the launches page. + # Uses the 2-hour TTL; failures are handled internally with cache fallback. + active_crewed = agenda.thespacedevs.get_active_crewed_flights(rocket_dir) # Always follow configured slugs follow_slugs: set[str] = set(config["FOLLOW_LAUNCHES"])