
Modular, client-agnostic HTTP toolkit offering type-safe error hierarchy, interceptor policies, automatic auth refresh, retry strategies, flexible request configs, multipart uploads and pluggable client implementations.
A modular, client-agnostic HTTP library for Kotlin Multiplatform projects. Write your HTTP code once, run it everywhere.
Author: Emmanuel Conradie
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// For Kotlin Multiplatform
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.blackarrows-apps:http-core:1.0.0")
implementation("io.github.blackarrows-apps:http-ktor:1.0.0")
}
}
}
}import io.blackarrows.http.ktor.di.httpModule
import io.blackarrows.http.providers.HeaderProvider
import io.blackarrows.http.providers.AuthRefresher
import org.koin.core.context.startKoin
import org.koin.dsl.module
startKoin {
modules(
module {
single<HeaderProvider> {
object : HeaderProvider {
override suspend fun getHeaders(vararg additional: Pair<String, String>): Map<String, String> {
return mapOf(
"Authorization" to "Bearer ${getToken()}",
*additional
)
}
override fun invalidate() {
// Clear cached headers if needed
}
}
}
single<AuthRefresher> {
object : AuthRefresher {
override suspend fun refreshToken(): Result<String> {
// Implement token refresh logic
return Result.success("new_token")
}
}
}
},
httpModule
)
}import io.blackarrows.http.io.HttpRequestExecutor
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
@Serializable
data class User(val id: Int, val name: String, val email: String)
class UserRepository(
private val httpExecutor: HttpRequestExecutor
) {
private val json = Json { ignoreUnknownKeys = true }
suspend fun getUsers(): Result<List<User>> {
return try {
val response = httpExecutor.getJson(
url = "https://api.example.com/users",
authRequired = true
)
val bodyString = response.body?.decodeToString() ?: "[]"
val users = json.decodeFromString(ListSerializer(User.serializer()), bodyString)
Result.success(users)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(user: User): Result<User> {
return try {
val response = httpExecutor.postJson(
url = "https://api.example.com/users",
body = user,
authRequired = true
)
val bodyString = response.body?.decodeToString() ?: throw IllegalStateException("Empty response")
val createdUser = json.decodeFromString(User.serializer(), bodyString)
Result.success(createdUser)
} catch (e: Exception) {
Result.failure(e)
}
}
}import io.blackarrows.http.errors.*
try {
val users = repository.getUsers()
// Success
} catch (e: AuthException) {
when (e.errorCode) {
ErrorCodes.AUTH_TOKEN_EXPIRED -> refreshTokenAndRetry()
ErrorCodes.AUTH_INSUFFICIENT_PERMISSIONS -> showPermissionDenied()
}
} catch (e: NetworkException) {
if (e.isRetryable) {
scheduleRetry()
} else {
showNetworkError()
}
} catch (e: HttpStatusException) {
when (e.statusCode) {
404 -> showNotFound()
429 -> handleRateLimiting()
in 500..599 -> showServerError()
}
}Client-agnostic abstractions and interfaces. Contains:
HttpRequestExecutor - Main interface for HTTP operationsHttpHeaders & HttpRequestConfig - Framework-independent typesApiResponse - Response wrapper with JSON deserializationZero implementation dependencies - Write your business logic once, swap clients anytime.
Ktor-based implementation with platform-specific optimizations:
Additional HTTP client implementations will be added based on community demand:
http-okhttp - Direct OkHttp implementation for Androidhttp-urlconnection - Pure Java URLConnection for lightweight JVM appshttp-js - JavaScript fetch API for Kotlin/JS targetsWant a specific client? Open an issue to request it!
| Platform | Status | Engine (Ktor) |
|---|---|---|
| Android | β | OkHttp |
| JVM | β | OkHttp |
| iOS arm64 | β | Darwin |
| iOS x64 | β | Darwin |
| iOS simulatorArm64 | β | Darwin |
Comprehensive exception hierarchy for precise error handling:
sealed class HttpException
βββ NetworkException // Network failures (retryable)
βββ AuthException // Auth/permission errors
βββ HttpStatusException // HTTP status codes (4xx, 5xx)
βββ TimeoutException // Request timeouts
βββ SerializationException // JSON parsing errorsEach exception includes:
Policy-based interceptor system for cross-cutting concerns:
val executor = KtorHttpRequestExecutor(
client = httpClient,
authHeaderProvider = headerProvider,
policies = listOf(
AuthPolicy(authRefresher, maxRetries = 2), // Automatic token refresh
RetryPolicy(maxRetries = 3, exponentialBackoff = true) // Retry transient failures
)
)AuthPolicy: Automatically refreshes expired tokens and retries requests RetryPolicy: Handles transient network failures with exponential backoff
All common HTTP operations with typed requests and responses:
// GET
executor.getJson(url, queryParams, headers, authRequired)
executor.getRaw(url, headers, queryParams, authRequired)
// POST
executor.postJson(url, body, headers, authRequired)
executor.postForm(url, formParams, headers, authRequired)
executor.postMultipart(url, multipartForm, headers, authRequired)
executor.postQuery(url, queryParams, contentType, headers, authRequired)
// PUT
executor.putJson(url, body, headers, authRequired)
executor.putRaw(url, body, contentType, headers, authRequired)
// DELETE
executor.deleteJson(url, body, headers, authRequired)
executor.deleteRaw(url, body, contentType, headers, authRequired)Simple multipart form data support:
val form = MultipartForm(
fields = mapOf("description" to "Profile picture"),
files = listOf(
MultipartPart(
name = "avatar",
value = imageBytes,
filename = "avatar.jpg",
contentType = "image/jpeg"
)
)
)
executor.postMultipart("https://api.example.com/upload", form, authRequired = true)Per-request configuration:
val config = HttpRequestConfig(
headers = HttpHeaders.of("X-Request-ID" to "12345"),
queryParams = mapOf("debug" to "true"),
timeout = 60_000L,
followRedirects = false
)
val response = executor.getJson(url, config = config)βββββββββββββββββββββββββββββββββββββββ
β Your Application Code β
β (Platform: Android/iOS/JVM) β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β http-core β
β β’ HttpRequestExecutor β
β β’ HttpHeaders, HttpRequestConfig β
β β’ Exception Hierarchy β
β β’ Interceptor System β
β β’ ApiResponse β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β http-ktor (or other) β
β β’ KtorHttpRequestExecutor β
β β’ Platform-Specific Engines β
β β’ JSON Serialization β
β β’ Error Mapping β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Platform HTTP Implementation β
β Android: OkHttp β
β iOS: Darwin (NSURLSession) β
β JVM: OkHttp β
βββββββββββββββββββββββββββββββββββββββ
Key Principle: Your application code depends only on http-core abstractions. The implementation module (http-ktor) is a runtime dependency that can be swapped without changing your business logic.
If you prefer not to use dependency injection:
import io.blackarrows.http.ktor.createHttpClient
import io.blackarrows.http.ktor.KtorHttpRequestExecutor
import io.blackarrows.http.providers.HeaderProvider
import io.blackarrows.http.io.interceptors.*
// 1. Create HTTP client
val httpClient = createHttpClient {
install(HttpTimeout) {
requestTimeoutMillis = 60_000
}
}
// 2. Create header provider
val headerProvider = object : HeaderProvider {
override suspend fun getHeaders(vararg additional: Pair<String, String>): Map<String, String> {
return mapOf("Authorization" to "Bearer ${tokenStore.getToken()}")
}
}
// 3. Create policies
val authPolicy = AuthPolicy(
authRefresher = object : AuthRefresher {
override suspend fun refresh(): ReauthResult {
return try {
tokenStore.refreshToken()
ReauthResult.Success
} catch (e: Exception) {
ReauthResult.Failed
}
}
}
)
val retryPolicy = RetryPolicy(maxRetries = 3)
// 4. Create executor
val httpExecutor = KtorHttpRequestExecutor(
client = httpClient,
authHeaderProvider = headerProvider,
policies = listOf(authPolicy, retryPolicy)
)Mock the HttpRequestExecutor interface for easy testing:
class MockHttpExecutor : HttpRequestExecutor {
var mockResponse: ApiResponse? = null
override suspend fun getJson(
url: String,
queryParams: Map<String, String>,
headers: HttpHeaders,
authRequired: Boolean,
config: HttpRequestConfig
): ApiResponse {
return mockResponse ?: error("No mock response configured")
}
// Implement other methods...
}
@Test
fun testRepository() = runTest {
val mockExecutor = MockHttpExecutor()
mockExecutor.mockResponse = MockApiResponse(
statusCode = 200,
body = """[{"id":1,"name":"Test"}]"""
)
val repository = UserRepository(mockExecutor)
val users = repository.getUsers()
assertEquals(1, users.size)
assertEquals("Test", users[0].name)
}Check out the sample module for a complete Android demo app that showcases:
The sample app demonstrates:
To run the sample app:
./gradlew :sample:assembleDebug
# or open in Android Studio and run the 'sample' configurationContributions are welcome! Please:
Want support for a different HTTP client? Open an issue with:
Client implementations will be prioritized based on community demand.
http-core)http-ktor)Copyright 2025 Emmanuel Conradie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Emmanuel Conradie GitHub: @E5c11
Built with β€οΈ for the Kotlin Multiplatform community
A modular, client-agnostic HTTP library for Kotlin Multiplatform projects. Write your HTTP code once, run it everywhere.
Author: Emmanuel Conradie
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// For Kotlin Multiplatform
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.blackarrows-apps:http-core:1.0.0")
implementation("io.github.blackarrows-apps:http-ktor:1.0.0")
}
}
}
}import io.blackarrows.http.ktor.di.httpModule
import io.blackarrows.http.providers.HeaderProvider
import io.blackarrows.http.providers.AuthRefresher
import org.koin.core.context.startKoin
import org.koin.dsl.module
startKoin {
modules(
module {
single<HeaderProvider> {
object : HeaderProvider {
override suspend fun getHeaders(vararg additional: Pair<String, String>): Map<String, String> {
return mapOf(
"Authorization" to "Bearer ${getToken()}",
*additional
)
}
override fun invalidate() {
// Clear cached headers if needed
}
}
}
single<AuthRefresher> {
object : AuthRefresher {
override suspend fun refreshToken(): Result<String> {
// Implement token refresh logic
return Result.success("new_token")
}
}
}
},
httpModule
)
}import io.blackarrows.http.io.HttpRequestExecutor
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
@Serializable
data class User(val id: Int, val name: String, val email: String)
class UserRepository(
private val httpExecutor: HttpRequestExecutor
) {
private val json = Json { ignoreUnknownKeys = true }
suspend fun getUsers(): Result<List<User>> {
return try {
val response = httpExecutor.getJson(
url = "https://api.example.com/users",
authRequired = true
)
val bodyString = response.body?.decodeToString() ?: "[]"
val users = json.decodeFromString(ListSerializer(User.serializer()), bodyString)
Result.success(users)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(user: User): Result<User> {
return try {
val response = httpExecutor.postJson(
url = "https://api.example.com/users",
body = user,
authRequired = true
)
val bodyString = response.body?.decodeToString() ?: throw IllegalStateException("Empty response")
val createdUser = json.decodeFromString(User.serializer(), bodyString)
Result.success(createdUser)
} catch (e: Exception) {
Result.failure(e)
}
}
}import io.blackarrows.http.errors.*
try {
val users = repository.getUsers()
// Success
} catch (e: AuthException) {
when (e.errorCode) {
ErrorCodes.AUTH_TOKEN_EXPIRED -> refreshTokenAndRetry()
ErrorCodes.AUTH_INSUFFICIENT_PERMISSIONS -> showPermissionDenied()
}
} catch (e: NetworkException) {
if (e.isRetryable) {
scheduleRetry()
} else {
showNetworkError()
}
} catch (e: HttpStatusException) {
when (e.statusCode) {
404 -> showNotFound()
429 -> handleRateLimiting()
in 500..599 -> showServerError()
}
}Client-agnostic abstractions and interfaces. Contains:
HttpRequestExecutor - Main interface for HTTP operationsHttpHeaders & HttpRequestConfig - Framework-independent typesApiResponse - Response wrapper with JSON deserializationZero implementation dependencies - Write your business logic once, swap clients anytime.
Ktor-based implementation with platform-specific optimizations:
Additional HTTP client implementations will be added based on community demand:
http-okhttp - Direct OkHttp implementation for Androidhttp-urlconnection - Pure Java URLConnection for lightweight JVM appshttp-js - JavaScript fetch API for Kotlin/JS targetsWant a specific client? Open an issue to request it!
| Platform | Status | Engine (Ktor) |
|---|---|---|
| Android | β | OkHttp |
| JVM | β | OkHttp |
| iOS arm64 | β | Darwin |
| iOS x64 | β | Darwin |
| iOS simulatorArm64 | β | Darwin |
Comprehensive exception hierarchy for precise error handling:
sealed class HttpException
βββ NetworkException // Network failures (retryable)
βββ AuthException // Auth/permission errors
βββ HttpStatusException // HTTP status codes (4xx, 5xx)
βββ TimeoutException // Request timeouts
βββ SerializationException // JSON parsing errorsEach exception includes:
Policy-based interceptor system for cross-cutting concerns:
val executor = KtorHttpRequestExecutor(
client = httpClient,
authHeaderProvider = headerProvider,
policies = listOf(
AuthPolicy(authRefresher, maxRetries = 2), // Automatic token refresh
RetryPolicy(maxRetries = 3, exponentialBackoff = true) // Retry transient failures
)
)AuthPolicy: Automatically refreshes expired tokens and retries requests RetryPolicy: Handles transient network failures with exponential backoff
All common HTTP operations with typed requests and responses:
// GET
executor.getJson(url, queryParams, headers, authRequired)
executor.getRaw(url, headers, queryParams, authRequired)
// POST
executor.postJson(url, body, headers, authRequired)
executor.postForm(url, formParams, headers, authRequired)
executor.postMultipart(url, multipartForm, headers, authRequired)
executor.postQuery(url, queryParams, contentType, headers, authRequired)
// PUT
executor.putJson(url, body, headers, authRequired)
executor.putRaw(url, body, contentType, headers, authRequired)
// DELETE
executor.deleteJson(url, body, headers, authRequired)
executor.deleteRaw(url, body, contentType, headers, authRequired)Simple multipart form data support:
val form = MultipartForm(
fields = mapOf("description" to "Profile picture"),
files = listOf(
MultipartPart(
name = "avatar",
value = imageBytes,
filename = "avatar.jpg",
contentType = "image/jpeg"
)
)
)
executor.postMultipart("https://api.example.com/upload", form, authRequired = true)Per-request configuration:
val config = HttpRequestConfig(
headers = HttpHeaders.of("X-Request-ID" to "12345"),
queryParams = mapOf("debug" to "true"),
timeout = 60_000L,
followRedirects = false
)
val response = executor.getJson(url, config = config)βββββββββββββββββββββββββββββββββββββββ
β Your Application Code β
β (Platform: Android/iOS/JVM) β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β http-core β
β β’ HttpRequestExecutor β
β β’ HttpHeaders, HttpRequestConfig β
β β’ Exception Hierarchy β
β β’ Interceptor System β
β β’ ApiResponse β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β http-ktor (or other) β
β β’ KtorHttpRequestExecutor β
β β’ Platform-Specific Engines β
β β’ JSON Serialization β
β β’ Error Mapping β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Platform HTTP Implementation β
β Android: OkHttp β
β iOS: Darwin (NSURLSession) β
β JVM: OkHttp β
βββββββββββββββββββββββββββββββββββββββ
Key Principle: Your application code depends only on http-core abstractions. The implementation module (http-ktor) is a runtime dependency that can be swapped without changing your business logic.
If you prefer not to use dependency injection:
import io.blackarrows.http.ktor.createHttpClient
import io.blackarrows.http.ktor.KtorHttpRequestExecutor
import io.blackarrows.http.providers.HeaderProvider
import io.blackarrows.http.io.interceptors.*
// 1. Create HTTP client
val httpClient = createHttpClient {
install(HttpTimeout) {
requestTimeoutMillis = 60_000
}
}
// 2. Create header provider
val headerProvider = object : HeaderProvider {
override suspend fun getHeaders(vararg additional: Pair<String, String>): Map<String, String> {
return mapOf("Authorization" to "Bearer ${tokenStore.getToken()}")
}
}
// 3. Create policies
val authPolicy = AuthPolicy(
authRefresher = object : AuthRefresher {
override suspend fun refresh(): ReauthResult {
return try {
tokenStore.refreshToken()
ReauthResult.Success
} catch (e: Exception) {
ReauthResult.Failed
}
}
}
)
val retryPolicy = RetryPolicy(maxRetries = 3)
// 4. Create executor
val httpExecutor = KtorHttpRequestExecutor(
client = httpClient,
authHeaderProvider = headerProvider,
policies = listOf(authPolicy, retryPolicy)
)Mock the HttpRequestExecutor interface for easy testing:
class MockHttpExecutor : HttpRequestExecutor {
var mockResponse: ApiResponse? = null
override suspend fun getJson(
url: String,
queryParams: Map<String, String>,
headers: HttpHeaders,
authRequired: Boolean,
config: HttpRequestConfig
): ApiResponse {
return mockResponse ?: error("No mock response configured")
}
// Implement other methods...
}
@Test
fun testRepository() = runTest {
val mockExecutor = MockHttpExecutor()
mockExecutor.mockResponse = MockApiResponse(
statusCode = 200,
body = """[{"id":1,"name":"Test"}]"""
)
val repository = UserRepository(mockExecutor)
val users = repository.getUsers()
assertEquals(1, users.size)
assertEquals("Test", users[0].name)
}Check out the sample module for a complete Android demo app that showcases:
The sample app demonstrates:
To run the sample app:
./gradlew :sample:assembleDebug
# or open in Android Studio and run the 'sample' configurationContributions are welcome! Please:
Want support for a different HTTP client? Open an issue with:
Client implementations will be prioritized based on community demand.
http-core)http-ktor)Copyright 2025 Emmanuel Conradie
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Emmanuel Conradie GitHub: @E5c11
Built with β€οΈ for the Kotlin Multiplatform community