Merge: YouTube client network-error handling (channel page 500 fix)

This commit is contained in:
npeter83 2026-07-09 09:42:21 +02:00
commit ca12b3bc9d

View file

@ -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)