Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.androidpoet:ktor-boost:$version")
}
}
}For typed WebSocket and SSE helpers, add the optional realtime module:
implementation("io.github.androidpoet:ktor-realtime:$version")Or pick protocol-specific modules:
implementation("io.github.androidpoet:ktor-realtime-websocket:$version") implementation("io.github.androidpoet:ktor-realtime-sse:$version") implementation("io.github.androidpoet:ktor-realtime-reverb:$version") implementation("io.github.androidpoet:ktor-realtime-socketio:$version") implementation("io.github.androidpoet:ktor-realtime-stomp:$version") implementation("io.github.androidpoet:ktor-realtime-graphql:$version") implementation("io.github.androidpoet:ktor-realtime-mqtt:$version") implementation("io.github.androidpoet:ktor-realtime-rsocket:$version") implementation("io.github.androidpoet:ktor-realtime-longpolling:$version")
- Simple
Result<T>wrappers for Ktor HTTP calls. - Typed
NetworkResult<T, E>for status codes, headers, raw error bodies, and decoded API errors. - Bearer token helpers with automatic refresh and request replay on
401. - Retry and timeout helpers for transient network failures.
- KMP-safe byte downloads with progress callbacks.
- Empty response support with
Unit. - Async helpers returning
Deferred<Result<T>>. - Request builder shortcuts for bearer auth, query params, JSON bodies, and form bodies.
- Suspend-friendly
Resulthelpers. - Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
- Optional
ktor-realtimemodule for typed WebSocket and SSE event flows. - Unified realtime endpoint model for WebSocket, SSE, Reverb, Socket.IO, STOMP, GraphQL subscriptions, MQTT-over-WS, RSocket, and long-polling adapters.
Use getResult, postResult, putResult, deleteResult, patchResult, headResult, or
optionsResult when you want a simple Kotlin Result<T>.
val result = httpClient.getResult<List<Movie>>("trendingMovies") result .onSuccess { movies -> // render movies } .onFailure { error -> // show error }
If you want non-2xx HTTP responses to become Result.failure(...), configure Ktor with
expectSuccess = true:
val httpClient = HttpClient { expectSuccess = true }
| Use case | API |
|---|---|
| Simple success/failure handling | getResult<T>() |
| Need status code, headers, or error body | getNetworkResult<T, E>() |
| Authenticated request with token refresh | getAuthenticatedNetworkResult<T, E>() |
| Retry transient failures | getResultWithRetry<T>() |
| Download bytes with progress | downloadBytes() |
Empty response body, such as 204 No Content |
deleteResult<Unit>() |
Need a Deferred<Result<T>> |
getResultAsync<T>() |
| Add common headers, query params, or body | bearerToken, queryParams, jsonBody, formBody |
Need suspend callbacks on Result |
onSuccessSuspend, onFailureSuspend, foldSuspend |
Use NetworkResult when your app needs response metadata or typed API errors.
Detailed operator docs: docs/network-result-operators.md.
val result = httpClient.getNetworkResult<User, ApiError>( urlString = "users/me", decodeErrorBody = { rawBody -> json.decodeFromString<ApiError>(rawBody) }, ) when (result) { is NetworkResult.Success -> { val user = result.body val statusCode = result.statusCode val headers = result.headers } is NetworkResult.HttpError -> { val statusCode = result.statusCode val rawErrorBody = result.rawBody val apiError = result.errorBody } is NetworkResult.ResponseDecodingError -> { val cause = result.cause } is NetworkResult.RequestError -> { val cause = result.cause } }
NetworkResult works with or without Ktor's expectSuccess setting.
Use Boost operators when you want a more expressive app-facing API:
httpClient.getResult<User, ApiError>("users/me", json) .onSuccess { result -> render(result.body) } .onUnauthorized { session.refreshToken() } .onRateLimited { rateLimit, _ -> scheduleRetry(rateLimit.retryAfterSeconds) } .recoverRequestError { cache.user() } .onError { result -> showMessage(result.messageOrNull ?: "Something went wrong") }
For realtime chat or presence streams, add ktor-realtime.
Detailed chat module docs: docs/chat-module.md.
httpClient.realtimeChat<ChatEvent, ChatCommand>( urlString = "wss://example.com/chat", onMessage = { event -> render(event) }, ) { sendJson(ChatCommand.Join(roomId = "general")) }
Protocol-neutral entrypoint:
val endpoint = RealtimeEndpoint.WebSocket("wss://example.com/realtime") httpClient.realtime<ChatEvent, ChatCommand>( endpoint = endpoint, onEvent = { event -> render(event) }, )
WebSocket, ServerSentEvents, Reverb, Socket.IO, STOMP, GraphQL subscriptions, MQTT-over-WS, RSocket, and long-polling are implemented with protocol-specific entrypoints.
Protocol-specific entrypoints are split as dedicated APIs (realtimeReverb, realtimeSocketIo, realtimeStomp, realtimeGraphQlSubscriptions, realtimeMqttOverWebSocket, realtimeRSocket, and realtimeLongPolling).
End-to-end realtime tests are available for WebSocket, SSE, and long-polling.
./gradlew realtimeIntegrationTest
This task:
- starts Docker Compose test infrastructure from
scripts/realtime-integration/docker-compose.yml - runs
:ktor-realtime:desktopIntegrationTest - tears down the containers
If Docker is not installed/running, use local unit tests instead:
./gradlew :ktor-realtime:desktopTest
You can also use convenience helpers:
val user = result.getOrNull() val apiError = result.errorOrNull() val statusCode = result.statusCodeOrNull() val displayName = result .map { user -> user.name } .getOrNull()
Use BearerTokenProvider when authenticated APIs need automatic token refresh. Your app owns
token storage; KtorBoost asks for the current token, refreshes when needed, and replays the
request after a 401.
class AppTokenProvider( private val tokenStore: TokenStore, private val authApi: AuthApi, ) : BearerTokenProvider { override suspend fun currentToken(): String? { return tokenStore.accessToken } override suspend fun refreshToken(): String? { val token = authApi.refreshAccessToken(tokenStore.refreshToken) tokenStore.accessToken = token return token } override suspend fun clearToken() { tokenStore.clear() } }
Then call authenticated helpers:
val result = httpClient.getAuthenticatedNetworkResult<User, ApiError>( urlString = "users/me", tokenProvider = appTokenProvider, decodeErrorBody = { rawBody -> json.decodeFromString<ApiError>(rawBody) }, )
For simple Result<T>:
val result = httpClient.getAuthenticatedResult<User>( urlString = "users/me", tokenProvider = appTokenProvider, )
Behavior:
- If
currentToken()returns a token, KtorBoost sendsAuthorization: Bearer <token>. - If
currentToken()returnsnull, KtorBoost callsrefreshToken()before the first request. - If the server returns
401, KtorBoost callsrefreshToken()and replays the request once. - If the replay still returns
401, KtorBoost callsclearToken()and returns the error. - KtorBoost does not cache tokens internally; your
BearerTokenProviderremains the source of truth.
Use retry helpers for transient failures such as 408, 429, 500, 502, 503, and 504.
val result = httpClient.getResultWithRetry<User>( urlString = "users/me", retryPolicy = RetryPolicy(maxRetries = 3), timeout = 5.seconds, )
For typed errors:
val result = httpClient.getNetworkResultWithRetry<User, ApiError>( urlString = "users/me", retryPolicy = RetryPolicy(maxRetries = 3), timeout = 5.seconds, decodeErrorBody = { rawBody -> json.decodeFromString<ApiError>(rawBody) }, )
Use downloadBytes for KMP-safe downloads with progress. The core API returns bytes and
metadata; apps can decide where to store the bytes on each platform.
val result = httpClient.downloadBytes( urlString = "files/report.pdf", onProgress = { progress -> val fraction = progress.fraction val bytesRead = progress.bytesRead val totalBytes = progress.totalBytes }, ) when (result) { is DownloadResult.Success -> { val bytes = result.content.bytes val contentType = result.content.contentType val contentLength = result.content.contentLength } is DownloadResult.HttpError -> { val statusCode = result.statusCode val rawErrorBody = result.rawBody } is DownloadResult.RequestError -> { val cause = result.cause } }
DownloadResult.Success includes:
bytes: downloadedByteArray.statusCode: HTTP status code.headers: response headers.contentLength: value fromContent-Length, when available.contentType: parsed response content type, when available.
DownloadProgress includes:
bytesRead: bytes received so far.totalBytes: total size when the server sendsContent-Length.fraction: progress from0.0to1.0when total size is known.
Use small request builder helpers to keep call sites readable.
val result = httpClient.postResult<User>("users") { bearerToken(token) queryParams(mapOf("source" to "android")) jsonBody(CreateUserRequest(name = "Ranbir")) }
For endpoints that return no body, request Unit.
val result = httpClient.deleteResult<Unit>("users/123")
Async helpers return Deferred<Result<T>>.
val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies") val result = deferredResult.await()
KtorBoost also includes suspend-friendly Result helpers:
result
.onSuccessSuspend { movies ->
repository.save(movies)
}
.onFailureSuspend { error ->
logger.log(error)
}val message = result.foldSuspend( onSuccess = { movies -> "Loaded ${movies.size} movies" }, onFailure = { error -> error.message ?: "Something went wrong" }, )
Existing simple helpers are still available:
getResultpostResultputResultdeleteResultpatchResultheadResultoptionsResultgetResultAsyncpostResultAsyncputResultAsyncdeleteResultAsyncpatchResultAsyncheadResultAsyncoptionsResultAsync
Recommended release version: 1.1.0.
This release adds NetworkResult, auth refresh helpers, retry helpers, request builder shortcuts,
downloads, and fixes coroutine behavior:
runCatchingSuspendnow rethrowsCancellationException.- Async helpers now return a real pending
Deferred<Result<T>>.
Contributions are welcome! If you've found a bug, have an idea for an improvement, or want to contribute new features, please open an issue or submit a pull request.
Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🤩
Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.
