From 0c612e19b45c8bfe5b96090339c6953e0fa0c7d7 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Wed, 5 Aug 2026 16:09:10 -0400 Subject: [PATCH] fix(espn): send an identifying User-Agent so ESPN stops returning 403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Around 11:00 EDT on 2026-08-04 ESPN's site.api began rejecting the agents this repo sends. Every scoreboard that goes through the shared data sources returned `403 Client Error: Forbidden` — standings, game summaries and scoreboards alike. A device that had been running fine logged 287 ESPN errors in a day. The filter is not the familiar one. Probing site.api across agents and libraries, using requests as the plugins do: bare 'LEDMatrix/1.0' 403 (with or without Accept) browser string 403 (no header rescues it) 'LEDMatrix/1.0 (+https://github.com/...)' 200 requests / urllib / curl defaults 200 So it rejects browser-style strings outright and bare custom tokens, and accepts honest client tokens or an agent that identifies the client and links to it. The instinct to "just send a browser User-Agent" is now exactly backwards — that is the one thing guaranteed to stay blocked. Both call sites here sent a bare token: `LEDMatrix/1.0` in the sports data sources and `LEDMatrix-Common/1.0` in the API helper. The Accept header the data sources already sent does not save it. Both now send an agent carrying the project URL, which also gives ESPN someone to contact rather than an anonymous token to rate-limit. Verified on a live device: ESPN errors went from a steady stream to zero across a restart, with live MLB games fetching again. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- src/base_classes/data_sources.py | 11 +++++++++-- src/common/api_helper.py | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base_classes/data_sources.py b/src/base_classes/data_sources.py index 777320ea3..065ea5770 100644 --- a/src/base_classes/data_sources.py +++ b/src/base_classes/data_sources.py @@ -44,9 +44,16 @@ def fetch_standings(self, sport: str, league: str) -> Dict: """Fetch standings for a sport/league.""" def get_headers(self) -> Dict[str, str]: - """Get headers for API requests.""" + """Get headers for API requests. + + The agent carries the project URL deliberately. Around 2026-08-04 ESPN + began returning 403 for bare custom tokens like 'LEDMatrix/1.0' — and + for browser-style strings — while accepting an agent that identifies + the client and links to it. An Accept header alone does not rescue the + bare form when the request goes out through requests. + """ return { - 'User-Agent': 'LEDMatrix/1.0', + 'User-Agent': 'LEDMatrix/1.0 (+https://github.com/ChuckBuilds/LEDMatrix)', 'Accept': 'application/json' } diff --git a/src/common/api_helper.py b/src/common/api_helper.py index 9d9b076f6..fa9b7694b 100644 --- a/src/common/api_helper.py +++ b/src/common/api_helper.py @@ -56,7 +56,9 @@ def __init__(self, cache_manager=None, default_timeout: int = 30, # Default headers self.session.headers.update({ - 'User-Agent': 'LEDMatrix-Common/1.0', + # Identifies the client and links to it: ESPN began 403ing bare + # custom tokens (and browser strings) around 2026-08-04. + 'User-Agent': 'LEDMatrix/1.0 (+https://github.com/ChuckBuilds/LEDMatrix)', 'Accept': 'application/json', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br',