KNETRateLimitHandler to inteligentny mechanizm obsługi rate limitów API.
Automatycznie parsuje nagłówki rate limit, obsługuje retry przy błędzie 429 (Too Many Requests),
i pozwala na pre-emptive throttling.
Używaj Rate Limit Handler gdy pracujesz z API, które ma limity zapytań (np. GitHub API, Twitter API, Stripe).
import rip.nerd.kitsunenet.util.KNETRateLimitHandler
// Utwórz handler
val handler = KNETRateLimitHandler()
// Wykonaj request z automatyczną obsługą rate limit
val response = handler.executeWithRateLimit(client, request)
// Handler automatycznie:
// ✅ Parsuje nagłówki X-RateLimit-* z odpowiedzi
// ✅ Retry przy 429 z odpowiednim opóźnieniem
// ✅ Respektuje nagłówek Retry-After
val handler = KNETRateLimitHandler.builder()
.maxRetries(3) // Maksymalna liczba prób
.defaultRetryDelayMs(1000) // Domyślne opóźnienie retry
.maxRetryDelayMs(60_000) // Maksymalne opóźnienie
.onRateLimited { host, delayMs ->
// Callback wywoływany przy rate limit
Log.w("API", "Rate limited na $host, czekam ${delayMs}ms")
analytics.track("rate_limited", mapOf("host" to host))
}
.build()
Handler automatycznie parsuje następujące nagłówki z odpowiedzi API:
| Nagłówek | Opis | Przykład |
|---|---|---|
X-RateLimit-Limit |
Maksymalna liczba zapytań w oknie czasowym | 1000 |
X-RateLimit-Remaining |
Pozostała liczba zapytań | 423 |
X-RateLimit-Reset |
Timestamp resetu limitu (Unix) | 1609459200 |
Retry-After |
Sekundy do ponowienia lub data HTTP | 60 lub Wed, 21 Oct 2025 07:28:00 GMT |
Handler obsługuje też alternatywne nazwy: RateLimit-* (IETF draft) i X-Rate-Limit-*.
executeWithRateLimit()Wykonuje request z automatycznym retry przy rate limit.
suspend fun executeWithRateLimit(
client: KNETClient,
request: KNETRequest
): KNETResponse
val handler = KNETRateLimitHandler(maxRetries = 5)
lifecycleScope.launch {
try {
val response = handler.executeWithRateLimit(client,
KNETRequest.get("https://api.github.com/users/octocat")
)
// Sukces - przetwórz odpowiedź
val user = response.json<GitHubUser>()
updateUI(user)
} catch (e: Exception) {
// Wszystkie retry wyczerpane lub inny błąd
showError("Nie udało się pobrać danych: ${e.message}")
}
}
executeWithPreemptiveThrottle()Czeka PRZED wysłaniem requestu jeśli limit jest niski.
suspend fun executeWithPreemptiveThrottle(
client: KNETClient,
request: KNETRequest,
minRemaining: Int = 1 // Czekaj jeśli remaining < minRemaining
): KNETResponse
// Czekaj jeśli pozostało mniej niż 10 zapytań
val response = handler.executeWithPreemptiveThrottle(
client,
request,
minRemaining = 10
)
// Dzięki temu unikasz błędu 429 - handler sam poczeka
// aż limit się zresetuje przed wysłaniem requestu
execute()Wykonuje request i zwraca Result (bez wyjątków).
suspend fun execute(
client: KNETClient,
request: KNETRequest
): Result
val result = handler.execute(client, request)
when (result) {
is KNETRateLimitHandler.Result.Success -> {
val response = result.response
val info = result.rateLimitInfo
Log.d("API", "Sukces! Pozostało: ${info?.remaining}/${info?.limit}")
processResponse(response)
}
is KNETRateLimitHandler.Result.RateLimited -> {
Log.w("API", "Rate limited! Retry za: ${result.retryAfterMs}ms")
showRateLimitWarning(result.retryAfterMs)
}
is KNETRateLimitHandler.Result.Error -> {
Log.e("API", "Błąd: ${result.exception.message}")
showError(result.exception)
}
}
getRateLimitInfo()Pobiera aktualne informacje o rate limit dla hosta.
fun getRateLimitInfo(host: String): RateLimitInfo?
val info = handler.getRateLimitInfo("api.github.com")
if (info != null) {
println("Limit: ${info.limit}")
println("Pozostało: ${info.remaining}")
println("Reset za: ${info.resetInSeconds} sekund")
println("Wykorzystanie: ${info.usagePercent}%")
println("Wyczerpany: ${info.isExhausted}")
// Pokaż w UI
progressBar.max = info.limit
progressBar.progress = info.remaining
resetTimeText.text = "Reset za ${info.resetInSeconds}s"
} else {
println("Brak danych o rate limit dla tego hosta")
}
canExecute()Sprawdza czy można wykonać request bez czekania.
fun canExecute(host: String): Boolean
if (handler.canExecute("api.example.com")) {
// Można wykonać natychmiast
val response = handler.executeWithRateLimit(client, request)
} else {
// Limit wyczerpany - pokaż komunikat
val waitTime = handler.estimateWaitTime("api.example.com")
showMessage("Proszę czekać ${waitTime / 1000} sekund")
}
estimateWaitTime()Szacuje czas oczekiwania do resetu limitu.
fun estimateWaitTime(host: String): Long // milisekundy
Struktura przechowująca informacje o rate limit:
data class RateLimitInfo(
val host: String, // Host API
val limit: Int, // Maksymalna liczba zapytań
val remaining: Int, // Pozostałe zapytania
val resetTimestamp: Long, // Timestamp resetu (ms)
val retryAfterMs: Long?, // Retry-After z 429 (opcjonalne)
val updatedAt: Long // Kiedy zaktualizowano
) {
val resetInSeconds: Long // Sekundy do resetu
val isExhausted: Boolean // Czy limit wyczerpany
val usagePercent: Double // % wykorzystania
val isFresh: Boolean // Czy dane są aktualne (<60s)
}
Gotowe konfiguracje dla typowych scenariuszy:
// Standardowy (3 retry, 1s opóźnienie)
val handler = KNETRateLimitHandler.standard()
// Agresywny (5 retry, 2s opóźnienie, max 2min)
val handler = KNETRateLimitHandler.aggressive()
// Bez retry (fail-fast)
val handler = KNETRateLimitHandler.noRetry()
| Preset | Max Retries | Default Delay | Max Delay | Użycie |
|---|---|---|---|---|
standard() |
3 | 1s | 60s | Większość API |
aggressive() |
5 | 2s | 120s | Ważne operacje |
noRetry() |
0 | - | - | Fail-fast |
class GitHubApiClient(
private val token: String
) {
private val client = KNETClient.builder()
.addInterceptor(KNETHeaderInterceptor.bearer(token))
.build()
private val rateLimitHandler = KNETRateLimitHandler.builder()
.maxRetries(3)
.onRateLimited { host, delay ->
Log.w("GitHub", "Rate limited, retry za ${delay}ms")
}
.build()
suspend fun getUser(username: String): GitHubUser {
val request = KNETRequest.get(
"https://api.github.com/users/$username"
)
val response = rateLimitHandler.executeWithRateLimit(client, request)
return response.json<GitHubUser>()
}
fun getRemainingRequests(): Int {
return rateLimitHandler
.getRateLimitInfo("api.github.com")
?.remaining ?: -1
}
}
class RateLimitViewModel : ViewModel() {
private val handler = KNETRateLimitHandler()
private val _rateLimitState = MutableStateFlow<RateLimitState>(RateLimitState.Unknown)
val rateLimitState = _rateLimitState.asStateFlow()
fun checkRateLimit(host: String) {
val info = handler.getRateLimitInfo(host)
_rateLimitState.value = when {
info == null -> RateLimitState.Unknown
info.isExhausted -> RateLimitState.Exhausted(info.resetInSeconds)
info.remaining < 10 -> RateLimitState.Low(info.remaining, info.limit)
else -> RateLimitState.Ok(info.remaining, info.limit)
}
}
}
sealed class RateLimitState {
object Unknown : RateLimitState()
data class Ok(val remaining: Int, val limit: Int) : RateLimitState()
data class Low(val remaining: Int, val limit: Int) : RateLimitState()
data class Exhausted(val resetInSeconds: Long) : RateLimitState()
}
suspend fun fetchAllUsers(userIds: List<String>): List<User> {
val handler = KNETRateLimitHandler()
val results = mutableListOf<User>()
for (userId in userIds) {
// Sprawdź czy możemy kontynuować
if (!handler.canExecute("api.example.com")) {
val waitTime = handler.estimateWaitTime("api.example.com")
Log.d("API", "Czekam ${waitTime}ms na reset limitu")
delay(waitTime)
}
val response = handler.executeWithRateLimit(
client,
KNETRequest.get("https://api.example.com/users/$userId")
)
results.add(response.json<User>())
}
return results
}
try {
val response = handler.executeWithRateLimit(client, request)
// Sukces
} catch (e: KNETError.HttpError) {
when (e.statusCode) {
429 -> {
// Wszystkie retry wyczerpane
showError("API limit wyczerpany. Spróbuj później.")
}
401 -> showError("Brak autoryzacji")
403 -> showError("Brak dostępu")
else -> showError("Błąd HTTP: ${e.statusCode}")
}
} catch (e: KNETError.NetworkError) {
showError("Błąd sieci: ${e.message}")
} catch (e: Exception) {
showError("Nieoczekiwany błąd: ${e.message}")
}