Error HTTP 429 Too Many Requests terjadi ketika aplikasi mobile mengirimkan request melebihi kuota rate limit yang ditentukan server dalam jendela waktu tertentu. Pada React Native, masalah ini umumnya dipicu oleh uncoordinated concurrent requests saat beberapa komponen me-mount data secara bersamaan, atau logika retry agresif saat koneksi internet pulih.
Solusi standar industri untuk mengatasi HTTP 429 melibatkan pembacaan header Retry-After, menunda eksekusi request berikutnya melalui antrean terpusat (centralized queue), dan menerapkan algoritma exponential backoff with full jitter jika header tersebut tidak disediakan oleh server.
Akar Masalah HTTP 429 pada React Native
Dua pola arsitektur yang sering memicu lonjakan request ke backend meliputi:
- Uncoordinated Concurrent Requests: Penggunaan beberapa custom hook (misalnya
useEffectganda pada dashboard yang memuat profil, notifikasi, feed, dan banner sekaligus) memicu burst 5-10 request HTTP secara simultan tanpa throttler. - Thundering Herd saat Reconnecting: Listener konektivitas seperti
@react-native-community/netinfoyang mengeksekusi ulang seluruh request gagal secara instan saat transisi dari offline ke online.
Parsing Header Retry-After: Delta-Seconds vs RFC 7231
Header Retry-After dikirim server dalam salah satu dari dua format spesifikasi HTTP:
- Delta-seconds: Bilangan bulat non-negatif yang menunjukkan durasi jeda dalam detik (contoh:
Retry-After: 120). - HTTP-date (RFC 7231 / RFC 9110): Tanggal absolut UTC kapan client diizinkan mencoba kembali (contoh:
Retry-After: Wed, 21 Oct 2025 07:28:00 GMT).
Parsing wajib menangani kedua format ini serta memperhitungkan risiko clock drift antara device client dan server.
/**
* Menghitung waktu tunggu dalam milidetik dari header Retry-After.
* Jika header absen/invalid, fallback ke Exponential Backoff + Full Jitter.
*/
export function calculateRetryDelay(
retryAfterHeader: string | null | undefined,
attempt = 1,
baseDelayMs = 1000,
maxDelayMs = 30000
): number {
if (retryAfterHeader) {
// 1. Cek format delta-seconds (integer murni)
if (/^\d+$/.test(retryAfterHeader.trim())) {
const seconds = parseInt(retryAfterHeader.trim(), 10);
return Math.max(0, seconds * 1000);
}
// 2. Cek format RFC 7231 HTTP-date
const targetDateEpoch = Date.parse(retryAfterHeader);
if (!Number.isNaN(targetDateEpoch)) {
const diffMs = targetDateEpoch - Date.now();
return Math.max(0, diffMs);
}
}
// Fallback: Full Jitter Exponential Backoff
// Sleep = rand(0, min(maxDelay, baseDelay * 2 ** attempt))
const calculatedBackoff = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt));
return Math.floor(Math.random() * calculatedBackoff);
}Implementasi Interceptor Axios dengan Antrean Terpusat
Untuk mencegah request non-kritis terus membanjiri server selama masa cooldown, client membutuhkan state rate limit global. Jika status 429 aktif, request yang masuk ditahan ke dalam memori queue sampai masa cooldown selesai.
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { calculateRetryDelay } from './calculateRetryDelay';
interface QueuedRequest {
resolve: (value?: unknown) => void;
reject: (reason?: any) => void;
}
export class ResilientApiClient {
private client: AxiosInstance;
private isPaused = false;
private queue: QueuedRequest[] = [];
private cooldownTimer: ReturnType<typeof setTimeout> | null = null;
constructor(baseURL: string) {
this.client = axios.create({ baseURL });
this.setupInterceptors();
}
private setupInterceptors() {
// Request Interceptor: Tahan request jika sedang cooldown
this.client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
if (this.isPaused) {
await new Promise((resolve, reject) => {
this.queue.push({ resolve, reject });
});
}
return config;
});
// Response Interceptor: Tangani 429
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const config = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
if (!error.response || error.response.status !== 429 || !config) {
return Promise.reject(error);
}
config._retryCount = (config._retryCount || 0) + 1;
if (config._retryCount > 3) {
return Promise.reject(error); // Batasi maksimal 3 retry per request
}
const retryAfterHeader = error.response.headers['retry-after'];
const delayMs = calculateRetryDelay(retryAfterHeader, config._retryCount);
this.pauseQueue(delayMs);
// Tunggu cooldown selesai sebelum retry request ini
await new Promise((resolve) => setTimeout(resolve, delayMs));
return this.client(config);
}
);
}
private pauseQueue(delayMs: number) {
if (this.isPaused) return;
this.isPaused = true;
if (this.cooldownTimer) clearTimeout(this.cooldownTimer);
this.cooldownTimer = setTimeout(() => {
this.isPaused = false;
const processingQueue = [...this.queue];
this.queue = [];
// Lepas semua antrean request yang ditahan
processingQueue.forEach((req) => req.resolve());
}, delayMs);
}
public getClient(): AxiosInstance {
return this.client;
}
}Unit Testing: Logika Parsing dan Backoff
Gunakan pengujian assert sederhana berikut untuk memvalidasi logika parsing tanpa ketergantungan pada runtime eksternal:
import { calculateRetryDelay } from './calculateRetryDelay';
function runTests() {
// 1. Test Delta-Seconds
const delayFromSeconds = calculateRetryDelay('60');
console.assert(delayFromSeconds === 60000, `Expected 60000, got ${delayFromSeconds}`);
// 2. Test HTTP-Date
const futureDate = new Date(Date.now() + 5000).toUTCString();
const delayFromDate = calculateRetryDelay(futureDate);
console.assert(
delayFromDate >= 4000 && delayFromDate <= 5000,
`Expected ~5000ms, got ${delayFromDate}`
);
// 3. Test Fallback Full Jitter (harus berada dalam batas interval)
const jitterDelay = calculateRetryDelay(null, 2, 1000, 10000);
const maxExpected = Math.min(10000, 1000 * Math.pow(2, 2)); // 4000
console.assert(
jitterDelay >= 0 && jitterDelay <= maxExpected,
`Jitter ${jitterDelay} out of bound [0, ${maxExpected}]`
);
console.log('Semua assertion berhasil dilewati.');
}
runTests();Pertimbangan Arsitektur dan Batasan
- Memory Footprint: Menyimpan terlalu banyak request tertunda di memori React Native berisiko memicu out-of-memory jika aplikasi menerima error 429 berkepanjangan. Berikan limit kapasitas antrean (misalnya drop request telemetry/analitik jika queue > 50 item).
- Prioritas Endpoint: Bedakan perlakuan antara endpoint non-kritis (misalnya prefetch gambar atau logging) dengan endpoint kritis (misalnya refresh auth token). Request non-kritis sebaiknya dibatalkan langsung menggunakan
AbortControllerdaripada ditahan di dalam antrean.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!