arrow-http
1.1.1indexedModular, 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.
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.1.1")
implementation("io.github.blackarrows-apps:http-ktor:1.1.1")
}
}
}
}
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()
} {
showNetworkError()
}
} (e: HttpStatusException) {
(e.statusCode) {
-> showNotFound()
-> handleRateLimiting()
-> 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!
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 errors
Each 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)
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:
Mock the HttpRequestExecutor interface for easy testing:
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' configuration
Contributions 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 the License.
You may obtain a copy of the License at
http:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
Emmanuel Conradie GitHub: @E5c11
Built with ❤️ for the Kotlin Multiplatform community
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
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)
}
}
}
| Platform | Status | Engine (Ktor) |
|---|
| Android | ✅ | OkHttp |
| JVM | ✅ | OkHttp |
| iOS arm64 | ✅ | Darwin |
| iOS x64 | ✅ | Darwin |
| iOS simulatorArm64 | ✅ | Darwin |
┌─────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────┘
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)
)
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...
}
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)
}
Surfaced from shared tags and platforms — no rankings paid for.