Skip to content

Polish video import: thumbnails, quota refunds, SSRF-hardened download#91

Merged
windoze95 merged 1 commit into
mainfrom
feat/video-polish
Jun 30, 2026
Merged

Polish video import: thumbnails, quota refunds, SSRF-hardened download#91
windoze95 merged 1 commit into
mainfrom
feat/video-polish

Conversation

@windoze95

Copy link
Copy Markdown
Owner

What

Production hardening for the two live video-import platforms (TikTok #89, Instagram #90), per the "polish what's live" direction. Three independent improvements, all backend-only:

1. Recipe thumbnails

Video imports had an empty imageUrl. Now a representative sampled frame (the middle one — past any intro, before any outro) is uploaded to S3 and set as the recipe's hero image. The URL is stored on VideoExtractionCache (additive column, dashboard-safe) so cache hits reuse the same image at no extra cost. Best-effort: an upload failure never fails the import.

2. Quota refunds on our-side failures

Video quota is consumed on acceptance (bounds concurrent-spam), but a job that later fails on our side (fetch error, frame sampling, extraction, at-capacity) now refunds the quota unit (DecrementUsage, floored at zero). A user never loses one of their monthly imports to an infra error — which the earlier Anthropic-credits outage showed is a real scenario.

3. SSRF-hardened media download

The frame sampler downloads a scraper-supplied (untrusted) media URL. Its HTTP client now blocks connections to private/internal IPs at dial time via net.Dialer.Control, which runs post-DNS-resolution — so it covers the initial host, every redirect hop, and DNS rebinding alike (the prior upfront ValidateExternalURL check was TOCTOU/redirect-blind).

Tests (offline, race-clean)

  • TestSafeDialControl — loopback/private/link-local/unspecified blocked; public allowed.
  • TestVideoImport_SetsThumbnail — middle frame uploaded, set on recipe + cached.
  • TestVideoImport_CacheHitReusesThumbnail — cache hit reuses the stored thumbnail.
  • TestVideoImport_RefundsQuotaOnFailure — failed import refunds quota (asserted via a lock-synchronized mock getter to avoid racing the async refund).

Schema

Adds thumbnail_url to video_extraction_caches (additive; AutoMigrate handles it). No changes to existing columns.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BU4UWZutHd1AnK3XAf7H19

Production hardening for the two live platforms (TikTok, Instagram):

- Thumbnail: a representative sampled frame (the middle one) is uploaded to
  S3 and set as the recipe's hero image (imageUrl was empty before). The URL
  is stored on the VideoExtractionCache (additive column) so cache hits reuse
  the same image at no extra cost. Best-effort — an upload failure never
  fails the import.
- Quota refund: video quota is consumed on acceptance to bound concurrency,
  but is now refunded (DecrementUsage, floored at zero) when the job later
  fails on our side, so a user never loses an import to an infra error.
- SSRF: the frame sampler's HTTP client now blocks connections to
  private/internal IPs at dial time via net.Dialer.Control, covering the
  initial host, every redirect hop, and DNS rebinding — the scraper-supplied
  media URL is untrusted.

Tests (offline, race-clean): safeDialControl block/allow table; thumbnail set
on fresh import + reused on cache hit; quota refunded on failure (via a
lock-synchronized mock getter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU4UWZutHd1AnK3XAf7H19
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@windoze95 windoze95 merged commit edf0ae4 into main Jun 30, 2026
1 check passed
@windoze95 windoze95 deleted the feat/video-polish branch June 30, 2026 05:26
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.

1 participant