Skip to content

Handoff audit: fix upstream-failure 500s + Safari/live bugs, add self-enforcing guardrails - #8

Closed
betmoar wants to merge 2 commits into
mainfrom
claude/codebase-handoff-audit-kduv4z
Closed

Handoff audit: fix upstream-failure 500s + Safari/live bugs, add self-enforcing guardrails#8
betmoar wants to merge 2 commits into
mainfrom
claude/codebase-handoff-audit-kduv4z

Conversation

@betmoar

@betmoar betmoar commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Full-codebase audit with remediation and permanent guardrails. Every fix has a regression test; the architecture invariants are now enforced by tests instead of prose.

Bug fixes (each verified with a reproducing test first)

Backend

  • Upstream failures no longer 500. pytubefix exceptions are mapped to YouTubeError in the adapter and handled globally: dead/private video → 404 VIDEO_UNAVAILABLE, anti-bot wall → 503 YOUTUBE_BLOCKED, pytubefix breakage → 502 UPSTREAM_FAILURE, YouTube unreachable → 502 UPSTREAM_UNREACHABLE. Previously /api/video, /proxy/audio|stream, and /api/hls all returned bare 500s on the most common runtime failures (expired cached itag, removed video). Replaces the dead HumError class.
  • HLS sidx parser crash. A sidx box extending past the 64 KB head fetch raised struct.error → 500. Now returns None → 415 → clean direct-stream fallback.
  • Finished live streams never ended. The tail-trim rewriter dropped #EXT-X-ENDLIST (it was queued as a per-segment tag with no following segment), so players polled a dead stream forever. It's now re-emitted after the kept segments.
  • SSRF via redirect. follow_redirects=True only allowlist-checked the initial URL; an httpx request event hook now enforces the YouTube host allowlist on every hop.
  • Non-ASCII bearer header → 401 instead of 500 (compare_digest on bytes).
  • Live master-manifest cache no longer grows unbounded (evict expired on write).
  • /api/debug/live/* (leaks raw CDN URLs) now requires DEBUG=true, not just the bearer.

Frontend

  • Safari replayed expired signed URLs after reload. hlsUrl survived queue persistence while audioUrl was stripped, so pickVodSrc() returned a dead URL and the rehydrate refetch never ran. Now stripped on flush and rehydrate.
  • Search race. Debounced typing fired overlapping requests with last-write-wins; a slow early query could overwrite newer results. Requests are now sequenced.
  • play() promise rejections caught (safePlay()); 20-listener audio event logger gated behind import.meta.env.DEV.

Guardrails & tooling

  • tests/unit/test_invariants.py — the three load-bearing rules enforced as tests: pytubefix imported only in the adapter, single httpx.AsyncClient, every route bearer-authed or signature-verified (route-table introspection), adapter never emits CDN URLs.
  • tests/unit/test_audit_regressions.py — one test per fix above, each commented with the failure it locks out.
  • scripts/check.sh — single pre-push gate mirroring CI exactly; removed the stale StreamTube-era dev-setup.sh (installed a nonexistent extra) and test.sh.
  • CI/release now lint tests too (ruff check ., with test-appropriate ignores; existing violations fixed).
  • Dockerfile runs as non-root; pytubefix's token-cache dir pre-created so enabling po_token later (the usual YouTube-block mitigation) doesn't crash.

Handoff docs

  • CLAUDE.md — mental model, load-bearing map, landmines, change-X-update-Y couplings.
  • docs/PLAYBOOKS.md — step-by-step procedures: "pytubefix broke", adding endpoints, adding signed-URL types, touching the stream cache or Player.
  • docs/BACKLOG.md — prioritized residual risks and debt (top item: CSP to neutralize the localStorage-token XSS exposure).
  • Fixed doc drift: repo links still pointing at betmoar/streamtube, stale test counts, routes.ts naming, CI listed as a non-goal despite existing.

Verification

./scripts/check.sh fully green: ruff clean (app + tests), mypy --strict clean, 157 backend tests (was 143), 148 frontend tests (was 146), svelte-check 0 errors, vite build OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EAANTDpjvHuz7DXt8M8dhZ


Generated by Claude Code

claude added 2 commits July 5, 2026 23:22
… check, stale hlsUrl

Backend:
- Map pytubefix exceptions to YouTubeError in the adapter and handle
  YouTubeError/UpstreamHostError/UpstreamStatusError/httpx.HTTPError
  globally: dead videos, expired itags, and YouTube outages now return
  404/502/503 JSON instead of bare 500s (replaces the dead HumError).
- sidx parser: return None (-> 415 -> direct-stream fallback) when the
  sidx box extends past the fetched head instead of raising struct.error.
- Live tail-trim rewriter: re-emit #EXT-X-ENDLIST after the kept segments
  so players stop polling finished broadcasts.
- Enforce the upstream host allowlist on every redirect hop via an httpx
  request event hook (SSRF hardening).
- Compare bearer tokens as bytes so non-ASCII header values 401, not 500.
- Evict expired live master-cache entries on write (unbounded growth).
- Gate /api/debug/live/* on DEBUG=true (it exposes raw CDN URLs).

Frontend:
- Strip signed hlsUrl from the persisted queue on flush AND rehydrate;
  Safari no longer replays an expired HLS URL after reload.
- Sequence search requests so a slow early query can't overwrite a newer one.
- Catch play() promise rejections (autoplay policy) via safePlay().
- Gate the 20-listener audio event logger behind import.meta.env.DEV.

Regression tests for each fix in tests/unit/test_audit_regressions.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAANTDpjvHuz7DXt8M8dhZ
- tests/unit/test_invariants.py: the three architecture invariants
  (pytubefix only in the adapter, single httpx.AsyncClient, every route
  bearer-authed or signature-verified, adapter never emits CDN URLs)
  enforced as tests.
- scripts/check.sh: single pre-push gate mirroring CI (backend ruff +
  mypy --strict + pytest, frontend svelte-check + vitest + build).
- Remove stale StreamTube-era scripts (dev-setup.sh installed a
  nonexistent extra; test.sh duplicated pytest).
- Lint tests in CI (ruff check . with test-appropriate ignores); fix
  the existing violations.
- Dockerfile: run as non-root user; pre-create pytubefix's token cache
  dir so enabling po_token later doesn't crash.
- CLAUDE.md, docs/PLAYBOOKS.md, docs/BACKLOG.md: maintainer handoff —
  mental model, landmines, couplings, step-by-step procedures for the
  recurring jobs, prioritized backlog + residual risk register. Fix doc
  drift (repo links, routes.ts name, stale test counts, CI non-goal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAANTDpjvHuz7DXt8M8dhZ
@betmoar betmoar closed this Jul 11, 2026
@betmoar
betmoar deleted the claude/codebase-handoff-audit-kduv4z branch July 11, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants