buildKHttpClient

fun buildKHttpClient(config: KHttpConfig): HttpClient(source)

Builds a pre-configured Ktor HttpClient following the Kindling convention.

Features

  • Base URL prefix on every request

  • JSON content negotiation (lenient: ignores unknown keys, coerces values)

  • Connect + request + socket timeouts

  • Structured error handling: 4xx → KHttpException.ClientError, 5xx → KHttpException.ServerError, unexpected status → KHttpException.NotAccepted

  • 401 differentiation: auth endpoints = wrong credentials, others = expired session

  • Auth header injection via KAuthProvider (Bearer, JWT, Basic, ApiKey, Custom) — also the correct place for global request headers (trace IDs, app version, etc.)

  • Automatic token refresh via KTokenRefresher, deduplicated by dev.kindling.utils.SingleFlight

  • Response-side hooks via KRequestInterceptor (analytics, metrics)

  • In-memory GET cache via KCacheConfig, backed by dev.kindling.utils.CircularBuffer and dev.kindling.utils.KMap

  • Optional cookie storage for cookie-based auth

  • Configurable logging

What is intentionally NOT in this client

  • Retry logic → use dev.kindling.utils.RetryRunner at the repository layer for per-call retry with observable state (isLoading, attempt, error).

  • Rate limiting → use dev.kindling.utils.Throttler or dev.kindling.utils.KThrottle at the ViewModel/UseCase layer for per-action throttling.

  • Request mutation → use KAuthProvider.Custom instead of a separate interceptor.

Plugin installation order (matters for 401 interception)

  1. ContentNegotiation — JSON (de)serialisation

  2. HttpTimeout — connect + request + socket timeouts

  3. Logging — structured request/response logging

  4. KRequestInterceptor — response-side hooks (analytics)

  5. KCache — in-memory GET cache

  6. KAuthProvider — auth header injection (also for global headers)

  7. KTokenRefresh — 401 intercept via Send hook + SingleFlight deduplication

  8. HttpCallValidator — 4xx/5xx → KHttpException (sees final response after retry)

  9. HttpCookies — optional cookie storage

Usage

// Standard Bearer JWT with auto-refresh and response analytics
val client = buildKHttpClient(
KHttpConfig(
baseUrl = "https://api.example.com/",
authProvider = KAuthProvider.Custom { request ->
session.accessToken.value?.let { request.bearerAuth(it) }
request.header("X-App-Version", BuildConfig.VERSION_NAME)
request.header("X-Trace-Id", UUID.randomUUID().toString())
},
tokenRefresher = KDefaultTokenRefresher(
refreshUrl = "https://auth.example.com/refresh",
getRefreshToken = { session.refreshToken.value },
onTokenRefreshed = { access, refresh -> session.saveTokens(access, refresh) },
onFailed = { session.clearSession() },
authProvider = authProvider,
),
interceptor = KRequestInterceptor(
onResponse = { analytics.track(it.status.value) },
),
cacheConfig = KCacheConfig(
maxAgeSeconds = 300,
strategy = KCacheStrategy.NetworkFirst,
),
onSessionExpired = { session.clearSession() },
onClientError = { code, msg -> toast("Error $code: $msg") },
onServerError = { code, msg -> toast("Server $code: $msg") },
)
)

// Repository layer: retry with RetryRunner
class ProductRepository(private val api: ProductAPI) {
private val retryRunner = RetryRunner<List<ProductDto>>(
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
retries = 3,
delay = 500.milliseconds,
backoffFactor = 2.0,
)
suspend fun getProducts() = retryRunner.run { api.getProducts() }
val isLoading = retryRunner.isLoading
}

// ViewModel layer: throttle user actions with Throttler
class SearchViewModel : ViewModel() {
private val throttler = KThrottle<String>(viewModelScope, 500.milliseconds) { query ->
repository.search(query)
}
fun onSearchInput(query: String) = throttler.emit(query)
}