From 5b81cca6846112814124f82a6f85645c555429fa Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:55:45 -0400 Subject: [PATCH] fix(espn): send an identifying User-Agent so ESPN stops returning 403 (#436) 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. Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 Co-authored-by: Claude Opus 5 (1M context) --- 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 777320ea..065ea577 100644 --- a/src/base_classes/data_sources.py +++ b/src/base_classes/data_sources.py @@ -44,9 +44,16 @@ class DataSource(ABC): """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 9d9b076f..fa9b7694 100644 --- a/src/common/api_helper.py +++ b/src/common/api_helper.py @@ -56,7 +56,9 @@ class APIHelper: # 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',