🔗 Interceptory

Interceptory pozwalają modyfikować requesty i response w pipeline. KNET oferuje 10+ gotowych interceptorów.

Jak działają?

// 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
Przepływ
Request → [Interceptor 1] → [Interceptor 2] → [Interceptor 3] → HTTP
                                                                    ↓
Response ← [Interceptor 1] ← [Interceptor 2] ← [Interceptor 3] ← HTTP

Dodawanie interceptorów

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()

📋 Lista interceptorów

KNETLoggingInterceptor

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

KNETHeaderInterceptor

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)

KNETRetryInterceptor

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)

KNETExponentialBackoffInterceptor

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

KNETCacheInterceptor

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)

KNETAuthInterceptor

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() }

KNETCircuitBreakerInterceptor

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

KNETRateLimitInterceptor

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)

KNETMetricsInterceptor

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())

KNETMockInterceptor

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}""")

KNETCompressionInterceptor

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

KNETDeduplicationInterceptor

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") }

Własny interceptor

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

⚠️ Ważne

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()

📚 Zobacz też