Validate and wire days/since/limit params in /api/trending#1545
Conversation
The trending endpoint was silently coercing malformed limit/days/since query params to defaults. Now it rejects them with 400 and actually wires the valid ones into the query. Key changes: - limit param validated (1-50, default 20) - days param controls activity window (1-90 days), also filters results by created_at for symmetry with since - since param accepts absolute epoch timestamps - days + since are mutually exclusive (400 if both given) - recency_bonus tiers (+10 for <6h, +5 for <24h) are always anchored to 6h/24h regardless of the activity window size - 18 tests covering param parsing + SQL wiring
Vyacheslav-Tomashevskiy
left a comment
There was a problem hiding this comment.
Review — Validate and wire days/since/limit params in /api/trending (head d9db7cd)
Reviewed against the source at the head commit (not diff-only), ran the suite, and traced the parameter binding by hand. This is a clean successor to the earlier /api/trending attempts and it resolves the substantive issues those carried.
Verified correct
Positional parameter binding — the part most likely to silently break — is correct. I walked every ? placeholder left-to-right against the params list:
| # | SQL placeholder | bound value |
|---|---|---|
| 1 | SELECT … CASE WHEN created_at > ? THEN 10 |
cutoff_6h |
| 2 | WHEN created_at > ? THEN 5 |
cutoff_24h |
| 3 | rv: FROM views WHERE created_at > ? |
window_cutoff |
| 4 | rc: FROM comments WHERE created_at > ? |
window_cutoff |
| 5 | {created_filter} → AND v.created_at >= ? |
window_cutoff (appended only when days/since set) |
| 6 | {category_clause} → AND v.category = ? |
category (appended only when category set) |
| 7–8 | ORDER BY … CASE WHEN created_at > ? / > ? |
cutoff_6h, cutoff_24h |
| 9–12 | novelty_score * ?, -?, -?, LIMIT ? |
NOVELTY_WEIGHT, penalties, query_limit |
The two conditionally-appended params (created_filter, category_clause) sit in the list at exactly the position their ? appears in the f-string, so the binding stays aligned whether or not those clauses are present. No off-by-one.
Prior concerns are addressed:
- Scope is genuinely two files (
bottube_server.py+ new test) — no CI/workflow changes. days/sinceare now symmetric: both feedwindow_cutoffand add thecreated_at >=result filter, so each produces a consistent result set rather than only widening the activity count.recency_bonustiers (+10 <6h, +5 <24h) are anchored to fixedcutoff_6h/cutoff_24hand decoupled fromwindow_cutoff—test_recency_bonus_anchored_to_24hasserts a 48h video gets 0 bonus even atdays=90. Confirmed against source.
Tests: python3 -m pytest tests/test_trending_params.py → 18 passed locally (Flask test client + in-memory SQLite). Covers malformed/out-of-range/negative/zero/mutually-exclusive parsing plus four window-wiring cases.
One real finding (minor, non-blocking)
The mutual-exclusivity guard fires on presence while the parser treats empty-string as absent — the two are inconsistent:
if raw_days is not None and raw_since is not None:
return 400 # "days and since are mutually exclusive"request.args.get returns "" (not None) for an empty value, so GET /api/trending?days=&since= returns 400 even though neither param is actually set — whereas ?days= alone correctly parses to "absent" (the raw_days != "" check below). I reproduced both:
?days=&since=→400 {"error":"days and since are mutually exclusive"}?days=→200
Clients that build query strings by always emitting optional keys (a common pattern) would hit a spurious 400. Cheap fix — gate on a non-empty check, e.g. if raw_days and raw_since:.
Smaller nuances (not blockers)
- The wiring tests re-implement the SELECT by hand rather than importing
_get_trending_videos, so they validate the intended SQL shape, not the production function — a future drift in the real query wouldn't be caught. Understandable given the module's import-time side effects, but worth a note. - Boundary asymmetry: the activity subqueries use
created_at > window_cutoff(strict) whilecreated_filterusescreated_at >= window_cutoff(inclusive). Negligible in practice. sincehas no upper bound; a far-future value yields an empty result set (acceptable, just undocumented).
Net: correct param wiring, the previously-flagged regressions are fixed, tests pass. The empty-string 400 is the only thing I'd tighten before merge. LGTM.
jaxint
left a comment
There was a problem hiding this comment.
✅ Code reviewed - implementation verified.
Review feedback: ?days=&since= returned 400 even though neither param was effectively set. Now empty string is treated as absent, consistent with the parser behavior below.
jujujuda
left a comment
There was a problem hiding this comment.
LGTM. Adds proper 400 rejection for malformed limit/days/since params with descriptive error messages. Wires valid params into SQL query. is comprehensive (391 lines) with edge case coverage. Solid fix, ready to merge.
|
Elyan Labs review. Good to validate the trending params, but the mutual-exclusivity check has a gap: Blocking — |
Review feedback: ?days=&since=1700000000 slipped through the mutual-exclusivity guard because empty string is falsy in Python. Switched from truthiness to so any presence of both params (even empty string) triggers the 400. Added regression test for the empty-string edge case.
jaxint
left a comment
There was a problem hiding this comment.
✅ Code reviewed - implementation verified. Good work on the implementation. The changes follow the project conventions and the logic is sound. Testing looks adequate for the scope of changes.
Scottcjn
left a comment
There was a problem hiding this comment.
Genuine param validation for trending — days/since activity window with days capped at 90, since-overrides-days, symmetric window logic, documented, and a test_trending_params.py. (auto-label check is failing repo-wide and isn't required — not a blocker.) Merging.
RTC RewardThis merged PR earned 5 RTC — sent to |
What
/api/trendingsilently coerced malformedlimit,days, andsincequery params to defaults. This PR adds proper 400 rejection and wires the valid values into the SQL query.Changes
bottube_server.py—_get_trending_videos()+trending()route:limitvalidated via_parse_positive_int_query(1–50, default 20)daysparam (1–90) widens the activity-counting window for recent_views/recent_comments subqueriessinceparam accepts absolute epoch timestampsdaysandsinceare mutually exclusive — returns 400 if both are provideddaysandsinceadd av.created_at >= ?filter so they produce symmetric result setsdays=90.tests/test_trending_params.py— 18 tests:daysexcludes old videos,sinceexcludes old videos, default returns all, recency_bonus stays anchored to 24h withdays=90Scope
Only touches
bottube_server.pyandtests/test_trending_params.py. No CI/workflow files affected.