Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

KtorBoost

LicenseAPIBuild StatusAndroid WeeklyProfile

Small Kotlin Multiplatform helpers that make Ktor client calls easier to return, inspect, and handle.

From try/catch to expressive success and error flows

Install

Maven Central

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

Features

  • 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 Result helpers.
  • Boost operators for typed HTTP errors, recovery helpers, and app-friendly failure messages.
  • Optional ktor-realtime module 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.

Quick Start

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
}

API Choices

Use caseAPI
Simple success/failure handlinggetResult<T>()
Need status code, headers, or error bodygetNetworkResult<T, E>()
Authenticated request with token refreshgetAuthenticatedNetworkResult<T, E>()
Retry transient failuresgetResultWithRetry<T>()
Download bytes with progressdownloadBytes()
Empty response body, such as 204 No ContentdeleteResult<Unit>()
Need a Deferred<Result<T>>getResultAsync<T>()
Add common headers, query params, or bodybearerToken, queryParams, jsonBody, formBody
Need suspend callbacks on ResultonSuccessSuspend, onFailureSuspend, foldSuspend

Typed HTTP Errors

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) {
isNetworkResult.Success-> {
val user = result.body
val statusCode = result.statusCode
val headers = result.headers
}
isNetworkResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
val apiError = result.errorBody
}
isNetworkResult.ResponseDecodingError-> {
val cause = result.cause
}
isNetworkResult.RequestError-> {
val cause = result.cause
}
}

NetworkResult works with or without Ktor's expectSuccess setting.

Boost Operators

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

Realtime Integration Tests

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

Auth Token Refresh

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.

classAppTokenProvider(
privatevaltokenStore:TokenStore,
privatevalauthApi:AuthApi,
) : BearerTokenProvider {
overridesuspendfuncurrentToken(): String? {
return tokenStore.accessToken
}
overridesuspendfunrefreshToken(): String? {
val token = authApi.refreshAccessToken(tokenStore.refreshToken)
tokenStore.accessToken = token
return token
}
overridesuspendfunclearToken() {
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 sends Authorization: Bearer <token>.
  • If currentToken() returns null, KtorBoost calls refreshToken() before the first request.
  • If the server returns 401, KtorBoost calls refreshToken() and replays the request once.
  • If the replay still returns 401, KtorBoost calls clearToken() and returns the error.
  • KtorBoost does not cache tokens internally; your BearerTokenProvider remains the source of truth.

Retry And Timeout

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

Downloads

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) {
isDownloadResult.Success-> {
val bytes = result.content.bytes
val contentType = result.content.contentType
val contentLength = result.content.contentLength
}
isDownloadResult.HttpError-> {
val statusCode = result.statusCode
val rawErrorBody = result.rawBody
}
isDownloadResult.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.

Request Builder Shortcuts

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

Empty Responses

For endpoints that return no body, request Unit.

val result = httpClient.deleteResult<Unit>("users/123")

Async

Async helpers return Deferred<Result<T>>.

val deferredResult = httpClient.getResultAsync<List<Movie>>("trendingMovies")
val result = deferredResult.await()

Suspend Result Helpers

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

Compatibility

Existing simple helpers are still available:

  • getResult
  • postResult
  • putResult
  • deleteResult
  • patchResult
  • headResult
  • optionsResult
  • getResultAsync
  • postResultAsync
  • putResultAsync
  • deleteResultAsync
  • patchResultAsync
  • headResultAsync
  • optionsResultAsync

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.
  • Async helpers now return a real pending Deferred<Result<T>>.

Contributing

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.

Find this repository useful? ❀️

Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🀩

License

Copyright 2023 AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0.
See LICENSE.txt for details.

About

πŸš€ Simplifying Ktor for Easier Development KMM/Compose Multiplatform

Topics

Resources

Code of conduct

Contributing

Stars

35 stars

Watchers

3 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages