
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("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")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 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 realtimeIntegrationTestThis task:
scripts/realtime-integration/docker-compose.yml
:ktor-realtime:desktopIntegrationTest
If Docker is not installed/running, use local unit tests instead:
./gradlew :ktor-realtime:desktopTestYou 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:
currentToken() 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.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: downloaded ByteArray.statusCode: HTTP status code.headers: response headers.contentLength: value from Content-Length, when available.contentType: parsed response content type, 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:
getResultpostResultputResultdeleteResultpatchResultheadResultoptionsResultgetResultAsyncpostResultAsyncputResultAsyncdeleteResultAsyncpatchResultAsyncheadResultAsyncoptionsResultAsyncRecommended 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>>.Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.
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")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 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 realtimeIntegrationTestThis task:
scripts/realtime-integration/docker-compose.yml
:ktor-realtime:desktopIntegrationTest
If Docker is not installed/running, use local unit tests instead:
./gradlew :ktor-realtime:desktopTestYou 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:
currentToken() 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.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: downloaded ByteArray.statusCode: HTTP status code.headers: response headers.contentLength: value from Content-Length, when available.contentType: parsed response content type, 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:
getResultpostResultputResultdeleteResultpatchResultheadResultoptionsResultgetResultAsyncpostResultAsyncputResultAsyncdeleteResultAsyncpatchResultAsyncheadResultAsyncoptionsResultAsyncRecommended 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>>.Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.