From 248782493c6c88f46f185dd3193105e3066fedde Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 9 Jul 2026 09:24:09 +0200 Subject: [PATCH 1/2] fix(youtube): map httpx transport errors to YouTubeError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The YouTubeClient issued httpx requests directly, so a network/transport failure (egress proxy unreachable, DNS, timeout, connection reset) escaped as a raw httpx.ConnectError. Callers only guard against YouTubeError, so such a fault propagated uncaught and surfaced as a 500 — e.g. opening an un-enriched channel's page (GET /api/channels/{id} lazily enriches About data) popped a blocking "Server error (500)" modal whenever the fixed-IP egress proxy was down. Route all client HTTP through a _send() helper that wraps httpx.HTTPError in YouTubeError, so every existing 'except YouTubeError' degrades gracefully: channel detail returns un-enriched (200), explore returns 422 (quiet), and scheduler jobs log-and-continue instead of crashing. --- backend/app/youtube/client.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/app/youtube/client.py b/backend/app/youtube/client.py index bf2293c..9695a55 100644 --- a/backend/app/youtube/client.py +++ b/backend/app/youtube/client.py @@ -55,6 +55,22 @@ class YouTubeClient: def __exit__(self, *exc) -> None: self._http.close() + # --- transport --- + def _send(self, method: str, url: str, **kwargs) -> httpx.Response: + """Issue an HTTP request, mapping any transport/network failure (egress proxy down, + DNS, timeout, connection reset) to YouTubeError. Every caller already handles + YouTubeError — degrade gracefully, log, and move on — so wrapping here keeps a + transient network fault (e.g. the fixed-IP egress proxy being unreachable) from + escaping as a raw httpx error and turning into an uncaught 500. HTTP status errors + are NOT touched here (we don't use raise_for_status): callers inspect resp.status_code + themselves and raise their own YouTubeError with the response body.""" + try: + return self._http.request(method, url, **kwargs) + except httpx.HTTPError as exc: + raise YouTubeError( + f"YouTube API unreachable ({exc.__class__.__name__}: {exc})" + ) from exc + # --- auth --- def _access_token(self) -> str: tok = self.user.token @@ -66,7 +82,8 @@ class YouTubeClient: refresh = decrypt(tok.refresh_token_enc) if not refresh: raise YouTubeError("No refresh token; user must re-authenticate") - resp = self._http.post( + resp = self._send( + "POST", GOOGLE_TOKEN_URL, data={ "client_id": sysconfig.get_str(self.db, "google_client_id"), @@ -94,7 +111,7 @@ class YouTubeClient: p["key"] = api_key else: headers["Authorization"] = f"Bearer {self._access_token()}" - resp = self._http.get(f"{API_BASE}/{path}", params=p, headers=headers) + resp = self._send("GET", f"{API_BASE}/{path}", params=p, headers=headers) quota.record_usage(self.db, cost) if resp.status_code != 200: log.warning("YouTube API %s -> %s: %s", path, resp.status_code, resp.text[:200]) @@ -210,7 +227,8 @@ class YouTubeClient: def delete_subscription(self, subscription_id: str) -> None: """Unsubscribe on YouTube. Requires the write scope and the user's OAuth token (never the public API key). subscriptions.delete costs 50 quota units.""" - resp = self._http.delete( + resp = self._send( + "DELETE", f"{API_BASE}/subscriptions", params={"id": subscription_id}, headers={"Authorization": f"Bearer {self._access_token()}"}, @@ -268,7 +286,7 @@ class YouTubeClient: # --- write endpoints (OAuth only, never the API key; each costs 50 quota units) --- def _write(self, method: str, path: str, *, params: dict, json: dict | None = None) -> dict: headers = {"Authorization": f"Bearer {self._access_token()}"} - resp = self._http.request( + resp = self._send( method, f"{API_BASE}/{path}", params=params, json=json, headers=headers ) quota.record_usage(self.db, 50) From 8675e2466307c6397b06cb3d16d42cac999f073b Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 9 Jul 2026 09:43:08 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=200.33.1=20=E2=80=94=20chan?= =?UTF-8?q?nel=20page=20network-error=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- frontend/src/lib/releaseNotes.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7d07a19..983a07e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.33.0 \ No newline at end of file +0.33.1 \ No newline at end of file diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index 109012a..5762aea 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,14 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.33.1", + date: "2026-07-09", + summary: "Fix: opening a channel no longer errors when YouTube is briefly unreachable.", + fixes: [ + 'Opening a channel page could pop a blocking "Server error" when the connection to YouTube was momentarily unavailable. The page now loads regardless — its latest channel details simply fill in once the connection is back — instead of failing.', + ], + }, { version: "0.33.0", date: "2026-07-09",