Fuel
HTTP networking library designed specifically for Kotlin. Idiomatic Kotlin API, extension function utilization, coroutines support. Provides simple and readable syntax, RxJava support, and testing assistance features. Achieves intuitive development experience leveraging Kotlin language characteristics.
GitHub Overview
kittinunf/fuel
The easiest HTTP networking library for Kotlin/Android
Topics
Star History
Library
Fuel
Overview
Fuel is "the easiest HTTP networking library for Kotlin/Android" designed as a modern HTTP client backed by Kotlinx Coroutines. With Kotlin Multiplatform support, it provides consistent HTTP communication APIs across JVM, Android, Apple, and WASM platforms. From simple and intuitive string extension functions to advanced FuelBuilder configurations, its flexible design allows you to choose the appropriate approach based on your project's scale and complexity. The coroutines-first design enables asynchronous, non-blocking HTTP processing in an idiomatic Kotlin way.
Details
Fuel 2025 edition has established its position as a Kotlin Multiplatform HTTP client library, with active development continuing at version 3.0.0-alpha04. It automatically selects optimized HTTP client implementations for each platform (OkHttpClient for JVM, NSURLSession for Apple platforms, Fetch API for WASM), absorbing platform differences. Three approaches - String extensions, Fuel object, and FuelBuilder - cover everything from simple one-off requests to complex enterprise-level HTTP communications requiring sophisticated configuration.
Key Features
- Multiplatform Support: Unified API across JVM, Android, Apple, and WASM
- Coroutines Native: Complete asynchronous support with Kotlinx Coroutines
- Flexible API Design: Graduated complexity from String extensions to FuelBuilder
- Automatic Platform Optimization: Auto-selection of optimal HTTP clients per environment
- Rich Serialization: Support for Jackson, Moshi, and Kotlinx Serialization
- API Routing: Structured API calls through FuelRouting interface
Pros and Cons
Pros
- Unified HTTP communication experience across Kotlin Multiplatform
- Extremely simple and intuitive API description through String extension functions
- Natural asynchronous programming with native Kotlinx Coroutines support
- Automatic selection of platform-specific optimized HTTP clients
- Graduated complexity design covering small projects to enterprise level
- Excellent integration with Kotlin ecosystem and idiomatic code
Cons
- Version 3.x is in alpha stage, requiring caution for production use
- Constraints with JVM-only serialization modules (Jackson, Moshi)
- Limited ecosystem compared to Ktor in fully multiplatform environments
- Restrictions in older environments due to Java 8 bytecode requirement
- Less abundant community resources compared to Requests due to being a newer library
- Potential for subtle behavioral differences across platforms
Reference Pages
Code Examples
Installation and Basic Setup
// build.gradle.kts (Common Kotlin)
dependencies {
implementation("com.github.kittinunf.fuel:fuel:3.0.0-alpha04")
implementation("com.github.kittinunf.fuel:fuel-coroutines:3.0.0-alpha04")
// Serialization (as needed)
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:3.0.0-alpha04")
implementation("com.github.kittinunf.fuel:fuel-jackson:3.0.0-alpha04") // JVM only
implementation("com.github.kittinunf.fuel:fuel-moshi:3.0.0-alpha04") // JVM only
}
// build.gradle (Android)
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
Basic HTTP Requests (GET/POST/PUT/DELETE)
import fuel.*
import kotlinx.coroutines.*
// Simplest GET request (String extension)
suspend fun simpleGet() {
val response: String = "https://api.example.com/users".httpGet().body.string()
println(response)
}
// GET request using Fuel object
suspend fun fuelObjectGet() {
val response = Fuel.get("https://api.example.com/users")
val stringData = response.body.string()
println(stringData)
}
// GET request with parameters
suspend fun getWithParameters() {
val params = listOf(
"page" to "1",
"limit" to "10",
"sort" to "created_at"
)
val response = Fuel.get("https://api.example.com/users", params).body.string()
println(response)
}
// POST request (sending JSON)
suspend fun postJson() {
val jsonData = """
{
"name": "John Doe",
"email": "[email protected]",
"age": 30
}
"""
val response = Fuel.post("https://api.example.com/users")
.header("Content-Type" to "application/json")
.header("Authorization" to "Bearer your-token")
.body(jsonData)
.body.string()
println("User created: $response")
}
// POST request (form data)
suspend fun postForm() {
val formData = listOf(
"username" to "testuser",
"password" to "secret123"
)
val response = "https://api.example.com/login".httpPost(formData).body.string()
println(response)
}
// PUT request (data update)
suspend fun putRequest() {
val updateData = """
{
"name": "Jane Doe",
"email": "[email protected]"
}
"""
val response = Fuel.put("https://api.example.com/users/123")
.header("Content-Type" to "application/json")
.header("Authorization" to "Bearer your-token")
.body(updateData)
.body.string()
println("User updated: $response")
}
// DELETE request
suspend fun deleteRequest() {
val response = Fuel.delete("https://api.example.com/users/123")
.header("Authorization" to "Bearer your-token")
if (response.statusCode == 204) {
println("User deleted successfully")
}
}
// Usage examples
fun main() = runBlocking {
simpleGet()
fuelObjectGet()
getWithParameters()
postJson()
postForm()
putRequest()
deleteRequest()
}
Advanced Configuration and Customization (Authentication, Timeout, Platform-specific Settings)
import fuel.*
import kotlinx.coroutines.*
// Custom configuration using FuelBuilder (JVM)
fun createCustomFuelJVM(): Fuel {
return FuelBuilder()
.config(
okhttp3.OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
)
.build()
}
// Custom configuration using FuelBuilder (Apple)
// expect/actual pattern for platform-specific implementations
expect fun createCustomFuelApple(): Fuel
// Apple implementation (iOS/macOS)
actual fun createCustomFuelApple(): Fuel {
val config = platform.Foundation.NSURLSessionConfiguration.defaultSessionConfiguration
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 60.0
return FuelBuilder()
.config(config)
.build()
}
// Authenticated requests
suspend fun authenticatedRequest() {
val headers = mapOf(
"Authorization" to "Bearer your-jwt-token",
"User-Agent" to "MyKotlinApp/1.0",
"Accept" to "application/json",
"X-API-Version" to "v2"
)
val response = Fuel.get("https://api.example.com/protected")
.header(headers)
.body.string()
println(response)
}
// Basic authentication
suspend fun basicAuth() {
val credentials = "username:password"
val encodedCredentials = credentials.encodeToByteArray().toBase64()
val response = Fuel.get("https://api.example.com/private")
.header("Authorization" to "Basic $encodedCredentials")
.body.string()
println(response)
}
// Custom headers and cookies
suspend fun customHeadersAndCookies() {
val customHeaders = mapOf(
"X-Custom-Header" to "custom-value",
"Accept-Language" to "en-US,ja-JP",
"Cache-Control" to "no-cache",
"Cookie" to "session_id=abc123; user_pref=dark_mode"
)
val response = Fuel.get("https://api.example.com/user-data")
.header(customHeaders)
.body.string()
println(response)
}
// Getting response details
suspend fun responseDetails() {
val response = Fuel.get("https://api.example.com/info")
println("Status Code: ${response.statusCode}")
println("Headers: ${response.headers}")
println("Content Type: ${response.headers["Content-Type"]}")
println("Content Length: ${response.headers["Content-Length"]}")
println("Body: ${response.body.string()}")
}
Error Handling and Retry Functionality
import fuel.*
import kotlinx.coroutines.*
// Comprehensive error handling
suspend fun safeRequest(url: String): String? {
return try {
val response = Fuel.get(url)
when (response.statusCode) {
in 200..299 -> response.body.string()
401 -> {
println("Authentication error: Please check your token")
null
}
403 -> {
println("Permission error: Access denied")
null
}
404 -> {
println("Not found: Resource does not exist")
null
}
429 -> {
println("Rate limit: Please wait before retrying")
null
}
in 500..599 -> {
println("Server error: ${response.statusCode}")
null
}
else -> {
println("Unexpected status: ${response.statusCode}")
null
}
}
} catch (e: Exception) {
println("Request error: ${e.message}")
null
}
}
// Request with retry functionality
suspend fun requestWithRetry(
url: String,
maxRetries: Int = 3,
backoffFactor: Long = 1000
): String? {
repeat(maxRetries) { attempt ->
try {
val response = Fuel.get(url)
if (response.statusCode in 200..299) {
return response.body.string()
}
// Retry for temporary errors
if (response.statusCode in listOf(429, 500, 502, 503, 504)) {
if (attempt < maxRetries - 1) {
val waitTime = backoffFactor * (1L shl attempt) // Exponential backoff
println("Attempt ${attempt + 1} failed (${response.statusCode}). Retrying in ${waitTime}ms...")
delay(waitTime)
return@repeat
}
}
println("Finally failed: ${response.statusCode}")
return null
} catch (e: Exception) {
if (attempt < maxRetries - 1) {
val waitTime = backoffFactor * (1L shl attempt)
println("Attempt ${attempt + 1} error: ${e.message}. Retrying in ${waitTime}ms...")
delay(waitTime)
} else {
println("Maximum attempts reached: ${e.message}")
return null
}
}
}
return null
}
// Timeout handling
suspend fun requestWithTimeout(url: String, timeoutMs: Long = 10000): String? {
return try {
withTimeout(timeoutMs) {
Fuel.get(url).body.string()
}
} catch (e: TimeoutCancellationException) {
println("Request timed out: ${timeoutMs}ms")
null
} catch (e: Exception) {
println("Request error: ${e.message}")
null
}
}
// Usage examples
suspend fun errorHandlingExamples() {
// Safe request
val data1 = safeRequest("https://api.example.com/data")
data1?.let { println("Success: $it") }
// Request with retry
val data2 = requestWithRetry("https://api.example.com/unstable")
data2?.let { println("Retry success: $it") }
// Request with timeout
val data3 = requestWithTimeout("https://api.example.com/slow", 5000)
data3?.let { println("Timeout success: $it") }
}
Concurrent Processing and Multiplatform Support
import fuel.*
import kotlinx.coroutines.*
// Parallel fetching of multiple URLs
suspend fun parallelRequests() {
val urls = listOf(
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments",
"https://api.example.com/categories"
)
val results = urls.map { url ->
async {
try {
val response = Fuel.get(url)
url to response.body.string()
} catch (e: Exception) {
url to "Error: ${e.message}"
}
}
}.awaitAll()
results.forEach { (url, result) ->
println("$url: ${result.take(100)}...")
}
}
// Pagination-aware fetch all data
suspend fun fetchAllPages(baseUrl: String, authToken: String): List<String> {
val allData = mutableListOf<String>()
var page = 1
var hasMore = true
while (hasMore) {
try {
val params = listOf("page" to page.toString(), "per_page" to "100")
val response = Fuel.get(baseUrl, params)
.header("Authorization" to "Bearer $authToken")
if (response.statusCode == 200) {
val data = response.body.string()
allData.add(data)
// Check for next page existence (by response headers or body)
hasMore = response.headers["X-Has-More"]?.firstOrNull() == "true" ||
data.contains("\"has_more\":true")
page++
println("Page $page completed")
// API load reduction
delay(100)
} else {
println("Error: ${response.statusCode}")
break
}
} catch (e: Exception) {
println("Error on page $page: ${e.message}")
break
}
}
println("Total pages fetched: ${allData.size}")
return allData
}
// Platform-specific functionality usage
// Common
expect class PlatformHttpClient {
suspend fun makeRequest(url: String): String
}
// JVM implementation
actual class PlatformHttpClient {
private val fuel = FuelBuilder()
.config(
okhttp3.OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("User-Agent", "MyApp-JVM/1.0")
.build()
chain.proceed(request)
}
.build()
)
.build()
actual suspend fun makeRequest(url: String): String {
return fuel.get(url).body.string()
}
}
// Apple implementation
actual class PlatformHttpClient {
private val fuel = FuelBuilder()
.config(
platform.Foundation.NSURLSessionConfiguration.defaultSessionConfiguration.apply {
timeoutIntervalForRequest = 30.0
HTTPAdditionalHeaders = mapOf(
"User-Agent" to "MyApp-iOS/1.0"
)
}
)
.build()
actual suspend fun makeRequest(url: String): String {
return fuel.get(url).body.string()
}
}
// HTTP client with caching functionality
class CachedHttpClient {
private val cache = mutableMapOf<String, Pair<String, Long>>()
private val cacheTimeout = 5 * 60 * 1000L // 5 minutes
suspend fun get(url: String, useCache: Boolean = true): String {
if (useCache) {
cache[url]?.let { (cachedData, timestamp) ->
if (System.currentTimeMillis() - timestamp < cacheTimeout) {
println("Cache hit: $url")
return cachedData
}
}
}
val response = Fuel.get(url).body.string()
cache[url] = response to System.currentTimeMillis()
return response
}
fun clearCache() {
cache.clear()
}
}
// Usage examples
suspend fun concurrencyExamples() {
// Execute parallel requests
parallelRequests()
// Fetch all pages
val allData = fetchAllPages("https://api.example.com/posts", "your-token")
// Platform-specific client
val platformClient = PlatformHttpClient()
val result = platformClient.makeRequest("https://api.example.com/platform-info")
// Cached client
val cachedClient = CachedHttpClient()
val data1 = cachedClient.get("https://api.example.com/slow-data") // Network
val data2 = cachedClient.get("https://api.example.com/slow-data") // Cache
}
API Design Patterns and Serialization Integration
import fuel.*
import kotlinx.coroutines.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
// Data class definitions
@Serializable
data class User(
val id: Int,
val name: String,
val email: String,
val age: Int
)
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val message: String? = null
)
// API design using FuelRouting
sealed class UserAPI : FuelRouting {
override val basePath = "https://api.example.com/v1"
object GetUsers : UserAPI() {
override val method = "GET"
override val path = "users"
override val parameters: List<Pair<String, String>>? = null
override val headers: Map<String, String> = mapOf(
"Accept" to "application/json"
)
override val body: String? = null
}
data class GetUser(val userId: Int) : UserAPI() {
override val method = "GET"
override val path = "users/$userId"
override val parameters: List<Pair<String, String>>? = null
override val headers: Map<String, String> = mapOf(
"Accept" to "application/json"
)
override val body: String? = null
}
data class CreateUser(val user: User) : UserAPI() {
override val method = "POST"
override val path = "users"
override val parameters: List<Pair<String, String>>? = null
override val headers: Map<String, String> = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json"
)
override val body: String = Json.encodeToString(user)
}
data class UpdateUser(val userId: Int, val user: User) : UserAPI() {
override val method = "PUT"
override val path = "users/$userId"
override val parameters: List<Pair<String, String>>? = null
override val headers: Map<String, String> = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json"
)
override val body: String = Json.encodeToString(user)
}
data class DeleteUser(val userId: Int) : UserAPI() {
override val method = "DELETE"
override val path = "users/$userId"
override val parameters: List<Pair<String, String>>? = null
override val headers: Map<String, String> = mapOf(
"Accept" to "application/json"
)
override val body: String? = null
}
}
// API client class
class UserApiClient(private val authToken: String) {
private val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}
suspend fun getUsers(): List<User>? {
return try {
val response = Fuel.request(UserAPI.GetUsers)
.header("Authorization" to "Bearer $authToken")
if (response.statusCode == 200) {
val responseData = json.decodeFromString<ApiResponse<List<User>>>(
response.body.string()
)
responseData.data
} else {
println("Error: ${response.statusCode}")
null
}
} catch (e: Exception) {
println("Get users error: ${e.message}")
null
}
}
suspend fun getUser(userId: Int): User? {
return try {
val response = Fuel.request(UserAPI.GetUser(userId))
.header("Authorization" to "Bearer $authToken")
if (response.statusCode == 200) {
val responseData = json.decodeFromString<ApiResponse<User>>(
response.body.string()
)
responseData.data
} else {
println("Error: ${response.statusCode}")
null
}
} catch (e: Exception) {
println("Get user error: ${e.message}")
null
}
}
suspend fun createUser(user: User): User? {
return try {
val response = Fuel.request(UserAPI.CreateUser(user))
.header("Authorization" to "Bearer $authToken")
if (response.statusCode == 201) {
val responseData = json.decodeFromString<ApiResponse<User>>(
response.body.string()
)
responseData.data
} else {
println("Error: ${response.statusCode}")
null
}
} catch (e: Exception) {
println("Create user error: ${e.message}")
null
}
}
suspend fun updateUser(userId: Int, user: User): User? {
return try {
val response = Fuel.request(UserAPI.UpdateUser(userId, user))
.header("Authorization" to "Bearer $authToken")
if (response.statusCode == 200) {
val responseData = json.decodeFromString<ApiResponse<User>>(
response.body.string()
)
responseData.data
} else {
println("Error: ${response.statusCode}")
null
}
} catch (e: Exception) {
println("Update user error: ${e.message}")
null
}
}
suspend fun deleteUser(userId: Int): Boolean {
return try {
val response = Fuel.request(UserAPI.DeleteUser(userId))
.header("Authorization" to "Bearer $authToken")
response.statusCode == 204
} catch (e: Exception) {
println("Delete user error: ${e.message}")
false
}
}
}
// File upload
suspend fun uploadFile(filePath: String, uploadUrl: String, authToken: String): Boolean {
return try {
// Multiplatform file reading
val fileBytes = readFileBytes(filePath) // Platform-specific implementation
val response = Fuel.post(uploadUrl)
.header("Authorization" to "Bearer $authToken")
.header("Content-Type" to "multipart/form-data")
.body(fileBytes)
response.statusCode in 200..299
} catch (e: Exception) {
println("File upload error: ${e.message}")
false
}
}
// Platform-specific file reading (expect/actual)
expect suspend fun readFileBytes(filePath: String): ByteArray
// Usage examples
suspend fun apiPatternExamples() {
val apiClient = UserApiClient("your-auth-token")
// Get user list
val users = apiClient.getUsers()
users?.forEach { user ->
println("User: ${user.name} (${user.email})")
}
// Create new user
val newUser = User(
id = 0,
name = "John Doe",
email = "[email protected]",
age = 30
)
val createdUser = apiClient.createUser(newUser)
createdUser?.let { user ->
println("Created user: ${user.name} (ID: ${user.id})")
// Update user
val updatedUser = user.copy(name = "Jane Doe")
apiClient.updateUser(user.id, updatedUser)
// Delete user
val deleted = apiClient.deleteUser(user.id)
if (deleted) {
println("User deleted successfully")
}
}
}