ux: clamp library feed pagination like sibling paginators
library_tab_feed used unwrap_or(1).max(1) with no upper bound, while
feed/discover/admin/issues all clamp(1, 1_000_000_000). A large ?page=
issued an unbounded SQL OFFSET deep-scan. Clamp it to match, and add a
regression test.
ultra-fuzz Run 5 M-UX1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 files changed,
+24 insertions,
-2 deletions
| 225 |
225 |
|
) -> Result<impl IntoResponse> {
|
| 226 |
226 |
|
use crate::templates::LibraryFeedTabTemplate;
|
| 227 |
227 |
|
|
| 228 |
|
- |
let page = query.page.unwrap_or(1).max(1);
|
| 229 |
|
- |
// Widen to i64 BEFORE multiplying to avoid u32 overflow on a large `?page=`.
|
|
228 |
+ |
// Clamp the upper bound like every sibling paginator (feed/discover/
|
|
229 |
+ |
// admin/issues): an unbounded `?page=` would issue a giant SQL OFFSET
|
|
230 |
+ |
// deep-scan. Widen to i64 BEFORE multiplying to avoid u32 overflow.
|
|
231 |
+ |
let page = query.page.unwrap_or(1).clamp(1, 1_000_000_000);
|
| 230 |
232 |
|
let offset = (page as i64 - 1) * constants::FEED_PAGE_SIZE as i64;
|
| 231 |
233 |
|
|
| 232 |
234 |
|
let total_items = db::follows::count_followed_feed_items(&state.db, user.id).await? as u32;
|
| 195 |
195 |
|
}
|
| 196 |
196 |
|
|
| 197 |
197 |
|
#[tokio::test]
|
|
198 |
+ |
async fn library_feed_huge_page_is_clamped_not_overflowing() {
|
|
199 |
+ |
// M-UX1: the feed tab's `?page=` must be clamped like every sibling
|
|
200 |
+ |
// paginator. A large in-range value (above the 1e9 clamp ceiling) should
|
|
201 |
+ |
// return a valid empty page, with the OFFSET bounded by the clamp rather
|
|
202 |
+ |
// than running a multi-billion-row deep-scan.
|
|
203 |
+ |
let mut h = TestHarness::new().await;
|
|
204 |
+ |
h.signup("feedpage", "feedpage@test.com", "password123").await;
|
|
205 |
+ |
|
|
206 |
+ |
let resp = h
|
|
207 |
+ |
.client
|
|
208 |
+ |
.get("/library/tabs/feed?page=4000000000")
|
|
209 |
+ |
.await;
|
|
210 |
+ |
assert_eq!(
|
|
211 |
+ |
resp.status, 200,
|
|
212 |
+ |
"large page should clamp to a valid empty page, got {} {}",
|
|
213 |
+ |
resp.status, resp.text
|
|
214 |
+ |
);
|
|
215 |
+ |
}
|
|
216 |
+ |
|
|
217 |
+ |
#[tokio::test]
|
| 198 |
218 |
|
async fn follow_nonexistent_rejected() {
|
| 199 |
219 |
|
let mut h = TestHarness::new().await;
|
| 200 |
220 |
|
let _user_id = h
|