Interceptory pozwalają modyfikować requesty i response w pipeline. KNET oferuje 10+ gotowych interceptorów.
// Interceptor to funkcja, która:
// 1. Otrzymuje chain (z requestem)
// 2. Może zmodyfikować request
// 3. Przekazuje dalej przez chain.proceed()
// 4. Otrzymuje response
// 5. Może go zmodyfikować
// 6. Zwraca response
fun interface KNETInterceptor {
suspend fun intercept(chain: Chain): KNETResponse
interface Chain {
val request: KNETRequest
suspend fun proceed(request: KNETRequest): KNETResponse
suspend fun proceed(): KNETResponse // używa chain.request
}
}
// Helper do interceptorów lambdowych
fun knetInterceptor(block: suspend (KNETInterceptor.Chain) -> KNETResponse): KNETInterceptor
Request → [Interceptor 1] → [Interceptor 2] → [Interceptor 3] → HTTP
↓
Response ← [Interceptor 1] ← [Interceptor 2] ← [Interceptor 3] ← HTTP
val client = KNETClient.builder()
.addInterceptor(KNETLoggingInterceptor())
.addInterceptor(KNETHeaderInterceptor.bearer(token))
.addInterceptor(KNETRetryInterceptor(maxRetries = 3))
.addInterceptor(KNETCacheInterceptor())
.build()
// Lub z KNETInterceptorChainBuilder (fluent DSL)
val client = KNETInterceptorChainBuilder()
.logBody()
.bearer(token)
.retry(maxRetries = 3)
.cache(ttlMs = 60_000)
.buildClient()
Loguje requesty i odpowiedzi do Logcat.
val interceptor = KNETLoggingInterceptor(
tag = "KNET",
level = KNETLoggingInterceptor.Level.BODY, // NONE, BASIC, HEADERS, BODY
logger = { msg -> Log.d("MyApp", msg) } // Opcjonalny: własny logger (jeden parametr)
)
// Metody fabryczne
KNETLoggingInterceptor.basic() // poziom BASIC
KNETLoggingInterceptor.full() // poziom BODY
// Przykład output na poziomie BODY:
// --> POST https://api.example.com/users
// Content-Type: application/json
// {"name":"Jan"}
// --> END POST
// <-- 201 Created (150ms)
// {"id":1,"name":"Jan"}
// <-- END HTTP
Dodaje nagłówki do każdego requestu.
// Bearer token
val interceptor = KNETHeaderInterceptor.bearer(token)
// Basic auth (Base64 encoded)
val interceptor = KNETHeaderInterceptor.basicAuth(username, password)
// Nagłówki JSON API (Accept + Content-Type: application/json)
val interceptor = KNETHeaderInterceptor.jsonApi()
// Custom User-Agent
val interceptor = KNETHeaderInterceptor.userAgent("MyApp/1.0")
// Własne nagłówki (statyczna mapa)
val interceptor = KNETHeaderInterceptor.custom(mapOf(
"X-API-Key" to apiKey,
"X-Client-Version" to BuildConfig.VERSION_NAME,
"Accept-Language" to Locale.getDefault().language
))
// Pojedynczy własny nagłówek
val interceptor = KNETHeaderInterceptor.custom("X-API-Key", apiKey)
Automatyczny retry przy błędach z exponential backoff.
val interceptor = KNETRetryInterceptor(
maxRetries = 3,
initialDelayMs = 1000,
maxDelayMs = 30_000,
multiplier = 2.0,
retryOnErrors = setOf(429, 500, 502, 503, 504),
retryOnNetworkError = true,
onRetry = { attempt, error, response ->
Log.d("Retry", "Próba $attempt, błąd: ${error?.message}")
}
)
// Metody fabryczne
KNETRetryInterceptor.simple(maxRetries = 3)
KNETRetryInterceptor.aggressive(maxRetries = 5)
Zaawansowany exponential backoff z jitter zapobiegający thundering herd.
val interceptor = KNETExponentialBackoffInterceptor(
maxRetries = 5,
initialDelayMs = 100, // 100ms
maxDelayMs = 30_000, // maks. 30s
multiplier = 2.0, // podwaja przy każdym retry
jitterFraction = 0.1, // 10% losowy jitter
retryableStatusCodes = setOf(408, 429, 500, 502, 503, 504)
)
// Opóźnienia: ~100ms → ~200ms → ~400ms → ~800ms → ~1600ms
Cache odpowiedzi w pamięci dla requestów GET/HEAD.
val interceptor = KNETCacheInterceptor(
maxAge = 300_000, // TTL 5 minut
maxSize = 100, // maks. 100 wpisów w cache
cacheableMethods = setOf("GET", "HEAD")
)
// Metody fabryczne
KNETCacheInterceptor.shortLived() // TTL 1 minuta
KNETCacheInterceptor.mediumLived() // TTL 5 minut
KNETCacheInterceptor.longLived() // TTL 1 godzina
// Nagłówek cache dodawany automatycznie:
// X-KNET-Cache: HIT (z cache)
// X-KNET-Cache: MISS (z sieci)
Autoryzacja z automatycznym odświeżaniem tokena przy 401.
val interceptor = KNETAuthInterceptor(
tokenProvider = { authRepository.getAccessToken() },
tokenRefresher = {
// Wywoływane automatycznie przy 401
authRepository.refreshToken()
authRepository.getAccessToken()
},
headerName = "Authorization",
headerPrefix = "Bearer ",
retryOnUnauthorized = true
)
// Ręczne zarządzanie tokenem
interceptor.setToken("nowy-token")
interceptor.clearToken() // Wymusza ponowne pobranie
// Metody fabryczne
KNETAuthInterceptor.withToken("statyczny-token")
KNETAuthInterceptor.withProvider { tokenStorage.get() }
Chroni przed kaskadowymi awariami blokując requesty do niestabilnych serwisów.
val interceptor = KNETCircuitBreakerInterceptor(
failureThreshold = 5, // Otwiera po 5 błędach
successThreshold = 2, // Zamyka po 2 sukcesach w HALF_OPEN
timeoutMs = 60_000, // Pozostaje OPEN przez 60s
failureStatusCodes = setOf(500, 502, 503, 504)
)
interceptor.currentCircuitState() // CLOSED, OPEN, HALF_OPEN
interceptor.reset() // Reset do CLOSED
Ogranicza częstotliwość requestów (rate limiting).
val interceptor = KNETRateLimitInterceptor(
maxRequests = 10,
perMillis = 1000, // 10 req/s
strategy = KNETRateLimitInterceptor.Strategy.DELAY // lub REJECT
)
// Metody fabryczne
KNETRateLimitInterceptor.perSecond(count = 10)
KNETRateLimitInterceptor.perMinute(count = 60)
Zbieranie metryk dla wszystkich requestów.
val collector = KNETMetricsCollector()
val interceptor = KNETMetricsInterceptor(collector)
// Późnej - pobierz statystyki
val stats = collector.getStats()
println("Łączne requesty: ${stats.totalRequests}")
println("Wskaźnik sukcesu: ${stats.successRate}%")
println("Śr. latencja: ${stats.averageLatencyMs}ms")
println(collector.formatStats())
Mockowanie odpowiedzi do testów. URL musi zaczynać się od prefiksu mock://.
val interceptor = KNETMockInterceptor.builder()
.addResponse("/users", """[{"id":1,"name":"Test"}]""", 200)
.addResponse("/orders", """{"id":2}""", 201)
.addErrorResponse("/error", 500, "Symulowany błąd")
.build()
// Użycie: wywołuj URL z mock://
val response = client.get("mock:///users")
// Prosty one-liner z domyślną odpowiedzią
val interceptor = KNETMockInterceptor.simple("""{"mock":true}""")
Automatycznie dodaje nagłówek Accept-Encoding i dekompresuje odpowiedzi gzip/deflate.
val interceptor = KNETCompressionInterceptor(
enableGzip = true,
enableDeflate = true
)
// Metody fabryczne
KNETCompressionInterceptor.gzipOnly()
KNETCompressionInterceptor.deflateOnly()
// Body odpowiedzi jest automatycznie dekompresowane
val response = client.get("https://api.example.com/large-data")
// response.bodyString jest już zdekompresowany
Zapobiega duplikatom równoczesnych requestów do tego samego URL.
val interceptor = KNETDeduplicationInterceptor(
ttlMs = 5000 // Okno deduplikacji: 5 sekund
)
// Te 3 równoczesne wywołania wykonają tylko 1 request sieciowy
launch { client.get("https://api.example.com/users") }
launch { client.get("https://api.example.com/users") }
launch { client.get("https://api.example.com/users") }
class MyCustomInterceptor : KNETInterceptor {
override suspend fun intercept(chain: KNETInterceptor.Chain): KNETResponse {
// 1. Przed requestem - dostęp przez chain.request
val request = chain.request
val startTime = System.currentTimeMillis()
val modifiedRequest = request.copy(
headers = request.headers + ("X-Custom" to "value")
)
// 2. Wykonaj request (przekaż zmodyfikowany request)
val response = chain.proceed(modifiedRequest)
// 3. Po response
val duration = System.currentTimeMillis() - startTime
Log.d("MyInterceptor", "${request.url} zajął ${duration}ms")
return response
}
}
// Lambdowy (z helperem knetInterceptor)
val interceptor = knetInterceptor { chain ->
val response = chain.proceed()
response
}
// Użycie
val client = KNETClient.builder()
.addInterceptor(MyCustomInterceptor())
.build()
Kolejność interceptorów ma znaczenie! Pierwszy dodany jest wykonywany pierwszy dla requestu i ostatni dla response.
// Zalecana kolejność:
val client = KNETClient.builder()
.addInterceptor(KNETLoggingInterceptor()) // 1. Logowanie (widzi wszystko)
.addInterceptor(KNETMetricsInterceptor(collector)) // 2. Metryki
.addInterceptor(KNETAuthInterceptor(tokenProvider)) // 3. Auth (przed retry)
.addInterceptor(KNETRetryInterceptor()) // 4. Retry
.addInterceptor(KNETCacheInterceptor()) // 5. Cache (może pominąć sieć)
.addInterceptor(KNETCompressionInterceptor()) // 6. Kompresja
.build()