KtorBoost
1.0.0indexedSimplifies HTTP request handling by packaging results in a Result class, improving error control and readability. Offers synchronous and asynchronous functions, enhancing code clarity and reducing duplication.
Simplifies HTTP request handling by packaging results in a Result class, improving error control and readability. Offers synchronous and asynchronous functions, enhancing code clarity and reducing duplication.
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()
implementation()
implementation()
implementation()
implementation()
implementation()
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 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 -> {
statusCode = result.statusCode
rawErrorBody = result.rawBody
apiError = result.errorBody
}
NetworkResult.ResponseDecodingError -> {
cause = result.cause
}
NetworkResult.RequestError -> {
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, , , , , and ).
End-to-end realtime tests are available for WebSocket, SSE, and long-polling.
./gradlew realtimeIntegrationTest
This task:
scripts/realtime-integration/docker-compose.yml:ktor-realtime:desktopIntegrationTestIf 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.
(
tokenStore: TokenStore,
authApi: AuthApi,
) : BearerTokenProvider {
: String? {
tokenStore.accessToken
}
: String? {
token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
token
}
{
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:
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
contentLength = result.content.contentLength
}
DownloadResult.HttpError -> {
statusCode = result.statusCode
rawErrorBody = result.rawBody
}
DownloadResult.RequestError -> {
cause = result.cause
}
}
DownloadResult.Success includes:
bytes: downloaded ByteArray.statusCode: HTTP status code.headers: response headers.contentLength: value from , when available.DownloadProgress includes:
bytesRead: bytes received so far.totalBytes: total size when the server sends Content-Length.fraction: progress from 0.0 to 1.0 when 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:
Recommended release version: 1.1.0.
This release adds NetworkResult, auth refresh helpers, retry helpers, request builder shortcuts,
downloads, and fixes coroutine behavior:
runCatchingSuspend now rethrows CancellationException.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. :star:
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.
Result<T> wrappers for Ktor HTTP calls.NetworkResult<T, E> for status codes, headers, raw error bodies, and decoded API errors.401.Unit.Deferred<Result<T>>.Result helpers.ktor-realtime module for typed WebSocket and SSE event flows.| 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 |
realtimeStomprealtimeGraphQlSubscriptionsrealtimeMqttOverWebSocketrealtimeRSocketrealtimeLongPollingcurrentToken() returns a token, KtorBoost sends Authorization: Bearer <token>.currentToken() returns null, KtorBoost calls refreshToken() before the first request.401, KtorBoost calls refreshToken() and replays the request once.401, KtorBoost calls clearToken() and returns the error.BearerTokenProvider remains the source of truth.Content-LengthcontentType: parsed response content type, when available.getResultpostResultputResultdeleteResultpatchResultheadResultoptionsResultgetResultAsyncpostResultAsyncputResultAsyncdeleteResultAsyncpatchResultAsyncheadResultAsyncoptionsResultAsyncSurfaced from shared tags and platforms — no rankings paid for.