fix: skip local quota spend on DNS/transport failures - #7
Conversation
Connection and name-resolution errors never reach Google, so recording them as playlistItems.insert spend emptied the local ledger during the 2026-08-01 outage. Keep the usage event for forensics at 0 units. Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughThe quota module now detects wrapped DNS, connection, TLS, and pre-response timeout failures. It records these failures with zero quota units and excludes them from ledger updates. Tests cover classification, HTTP 503 distinction, and repeated DNS failures. ChangesYouTube transport failure accounting
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant YouTubeRequest
participant classify_result
participant UsageRecording
participant QuotaLedger
YouTubeRequest->>classify_result: raised DNS or other transport exception
classify_result->>classify_result: traverse exception chain
classify_result-->>UsageRecording: transport_error without HTTP status
UsageRecording->>UsageRecording: record zero quota units
UsageRecording-->>QuotaLedger: skip ledger update
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/core/youtube_quota.py (1)
154-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the dead
requests.exceptions.*extension; a bareOSErrorcheck already covers it.
requests.exceptions.RequestExceptionsubclassesIOError(an alias ofOSErrorin Python 3), andConnectionError,Timeout,ConnectTimeout,ReadTimeout, andSSLErrorall derive fromRequestException. Built-inConnectionErrorandTimeoutErrorare alsoOSErrorsubclasses. Sotransport_types = (ConnectionError, TimeoutError, OSError)already reduces toOSError, and the localimport requestsplus the tuple extension at lines 162-168 add no new matches —isinstance(exc, OSError)alone already catches every listedrequests.exceptions.*type.This dead block is also the source of all three static-analysis findings (RUF005 concatenation, S110 try-except-pass, BLE001 blind except). Removing it fixes all three at once. Keep the name-based fallback at lines 174-183: urllib3-native classes like
MaxRetryErrorandNameResolutionErrorderive from urllib3'sHTTPError(Exception), notOSError, so that part is still load-bearing.♻️ Proposed simplification
- transport_types = ( - ConnectionError, - TimeoutError, - OSError, - ) - try: - import requests - - transport_types = transport_types + ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - requests.exceptions.ConnectTimeout, - requests.exceptions.ReadTimeout, - requests.exceptions.SSLError, - ) - except Exception: - pass + # ConnectionError/TimeoutError are OSError subclasses, and every + # requests.exceptions.* transport error subclasses OSError via + # RequestException(IOError), so a single OSError check covers them all. + transport_types = (OSError,)Since this depends on the exception hierarchy of
requestsand CPython'sOSError/IOErroraliasing, please confirm this holds for therequestsversion pinned in this repository before merging the simplification.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/core/youtube_quota.py` around lines 154 - 170, Remove the local requests import, broad exception handler, and transport_types tuple extension in the transport exception setup. Keep transport_types as the built-in (effectively OSError) types and preserve the existing name-based fallback for urllib3-native exceptions such as MaxRetryError and NameResolutionError. Confirm the pinned requests version retains the stated RequestException hierarchy before applying the simplification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/core/youtube_quota.py`:
- Around line 154-170: Remove the local requests import, broad exception
handler, and transport_types tuple extension in the transport exception setup.
Keep transport_types as the built-in (effectively OSError) types and preserve
the existing name-based fallback for urllib3-native exceptions such as
MaxRetryError and NameResolutionError. Confirm the pinned requests version
retains the stated RequestException hierarchy before applying the
simplification.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 254a0b59-cee8-4b63-9d02-752d6120024e
📒 Files selected for processing (2)
backend/core/youtube_quota.pybackend/tests/test_youtube_quota.py
Co-authored-by: Cursor <cursoragent@cursor.com>
…ate HTTP calls, gate PostDetail taxonomy behind connectivity Port reviewed fixes from upstream PR #7 (draft/android-category-sync @ 5975a02): - PostDetailScreen: setEditedCategory(cat.id) not cat.name — upstream backend stores categories by id (lowercase slug), not display name - PostDetailScreen: gate taxonomy fetch behind testConnection() to avoid wasted 404 round-trip on upstream servers without GET /taxonomy - HomeScreen: loadCategories() accepts optional pre-fetched taxonomy param; post-sync reload reuses cached payload instead of redundant HTTP call - api.ts: getTaxonomy() uses shared TaxonomyPayload type from taxonomySupport.ts TypeScript passes. No scope change — same 8 superbrain-app/ files.
Port reviewed fixes from upstream PR #7 (draft/android-category-sync @ 878a1dc, d629ccf, 3acb694) that landed on the draft branch after c195a38 already ported its earlier fixes. draft stays on the old upstream base to keep that PR a clean Android-only diff, so these never reached main: - database.py: get_posts_since() now orders by (updated_at, shortcode) and fetches limit+1/trims to compute real has_more — updated_at alone isn't a stable OFFSET cursor when timestamps tie, which could skip or duplicate rows across pages. - api.py /sync: has_more/next_offset now come from the db layer instead of the len(results) == limit heuristic. - syncService.ts deltaSync: hard page cap + non-advancing-cursor detection so an offset-blind server can't spin the loop forever, and the sync cursor only advances when pagination genuinely completed (not on page-cap/non-advancing/fetch-failure early exit) — otherwise the next sync would silently skip whatever changes existed past the stop point. - api.ts syncPosts: distinguishes a transient fetch failure (`failed: true`) from a legitimate empty/final page; getTaxonomy()'s response type now matches TaxonomyPayload's optional precedence/guidance. - HomeScreen.tsx: taxonomyRef (component-level, reset at the top of every loadPosts call) replaced with a per-call local plus a promise shared between loadCategories/loadPosts at bootstrap — fixes both the redundant double /taxonomy fetch and the race between overlapping loadPosts calls (focus listener vs. poll refresh) stomping each other's cache. TypeScript passes; backend/tests/test_database_concurrency.py updated for the new (results, has_more) return shape. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
transport_errorwithunits=0for forensics.Reproduces/fixes the 2026-08-01 incident where ~200 OAuth DNS failures burned the local day budget.
Test plan
python3 -m unittest tests.test_youtube_quota~/.superbrain-serverand restart playlist sync when idleMade with Cursor
Summary by CodeRabbit