Limit 100 Diam-diam Jadi 20, Daftarnya 54
Kontrak limit lebih penting daripada angka yang dikirim klien. Ketika limit 100 diam-diam menjadi 20, daftar 54 kartu hanya menampilkan 20 di layar.
Ringkasan
Ada 54 organisasi olahraga. Tapi di layar hanya muncul 20. Perbaikannya adalah mengambil data per halaman penuh.
Angka di atas daftar menunjukkan 54 organisasi olahraga. Kartu yang benar-benar muncul di layar: 20. Portal KotaPortal memang menampilkan counter dari total yang dilaporkan server, dan angka itu memang 54. Dugaan pertama waktu itu biasa: halaman gagal mengambil semua data, jadi batas permintaan dinaikkan sampai habis. Angkanya sudah limit: 100, seharusnya lebih dari cukup.
Ternyata permintaan itu tidak pernah diterima utuh.
Counter bilang 54, layar cuma 20
Service yang melayani daftar itu ada di api/internal/organisasi/service.go, dan dua fungsinya, List serta ListAdmin, memuat baris yang sama:
if q.Limit < 1 || q.Limit > 50 {
q.Limit = 20
}
limit: 100 di atas 50, lalu jatuh ke 20. Bukan ke 50, dan tidak ada error: respons tetap 200 OK dengan 20 baris data. Kontrak OpenAPI untuk daftar publik sudah menuliskan batasnya sejak awal, { name: limit, schema: { type: integer, default: 20, maximum: 50 } } — angka 100 memang di luar kontrak, dan server memilih menurunkan diam-diam alih-alih menolak.
Perilaku seperti ini bukan keanehan satu proyek. AIP-158 menyebut dua hal yang persis terjadi di sini: nilai page_size di atas batas "coerce down to the maximum permitted page size" [1], dan sebuah API berhak "return fewer results than the number requested" [1]. GitHub REST juga bekerja dengan cara yang sama, "GitHub will paginate the results and return a subset of the results" [2], lalu halaman berikutnya diambil dari header link, bukan dari membesarnya per_page. Artinya, angka yang dikirim klien memang cuma angka harapan.
Padahal responsnya sendiri sudah jujur. Handler meneruskan limit yang sudah di-clamp ke dalam meta:
response.Paginated(w, data, int64(listQ.Page), int64(listQ.Limit), int64(total))
Jadi meta.limit bernilai 20, bukan 100. Klien menerima jawaban yang benar, lalu mengabaikannya.
Baca responsnya, bukan permintaannya
Perbaikannya ada di frontend/src/components/features/organisasi/cabor-list.tsx. Alih-alih satu request besar, komponen sekarang mengambil per halaman penuh dan berhenti di tiga kondisi:
const PAGE_SIZE = 50;
for (let page = 1; ; page++) {
// filter lain (kategori, tipe, pencarian) ikut di setiap request
const res = await listOrganizations({ page, limit: PAGE_SIZE });
if (cancelled) return;
const batch = res?.data ?? [];
accumulated.push(...batch);
totalCount = res?.meta?.total ?? accumulated.length;
if (accumulated.length >= totalCount || batch.length < PAGE_SIZE) break;
}
Kondisi pertama berhenti saat akumulasi sudah sampai meta.total. Kondisi kedua menangkap halaman terakhir yang isinya kurang dari PAGE_SIZE, dan sekaligus menjadi pengaman kalau meta.total ternyata tidak konsisten dengan isi respons. Kondisi ketiga, halaman kosong, memastikan loop tidak berjalan tanpa batas ketika filter berubah di tengah pencarian. Untuk 54 baris, dua request selesai sudah.
Loop multi-request seperti ini punya masalah lain: pengguna bisa berpindah tab atau mengetik lagi sebelum request pertama selesai. Solusinya satu flag di cleanup effect:
let cancelled = false;
// ... setState selalu dijaga if (cancelled) return;
return () => { cancelled = true; clearTimeout(timer); };
React menjalankan cleanup sebelum setup berikutnya, dan di pengembangan Strict Mode bahkan menjalankan satu siklus setup+cleanup tambahan [3]. Tanpa flag itu, data tab lama bisa menimpa tab baru setelah semuanya selesai. Debounce 350 ms tetap dipertahankan karena pencarian tidak boleh membanjiri server dengan request per huruf.
Gejalanya gampang dicek tanpa membuka source code. Buka tab Network di browser, kirim permintaan daftar dengan limit besar, lalu bandingkan tiga angka: panjang array data, meta.limit, dan meta.total. Kalau panjang data sama dengan meta.limit sedangkan total jauh lebih besar, permintaan itu memang sudah dikembalikan ke batas server, dan satu-satunya jalan keluar adalah mengikuti halaman yang disediakan. Kasus yang sama muncul di hampir semua daftar yang difilter, termasuk pencarian yang mengetik per huruf.
Pelajaran yang dipetik bukan soal angka 20 atau 50. meta.total adalah petunjuk, bukan tanda terima; dan sebuah limit adalah kontrak antara klien dan server, bukan harapan pribadi. Next.js menyebut pola ini sebagai masalah client data waterfall yang sulit dikelola [4], dan untuk dataset yang jauh lebih besar jawabannya memang SWR atau TanStack Query, bukan loop manual. Untuk daftar puluhan baris, dua request yang berhenti di tempat yang tepat adalah alat yang lebih kecil dan lebih jujur.