A full KMP port of NiceHttp that runs on every Kotlin target: JVM, Android, JS (browser + Node.js), WASM/JS, iOS, macOS, tvOS, watchOS, Linux, and Windows.
| Target(s) | Ktor engine |
|---|---|
| JVM | OkHttp |
| Android | OkHttp |
| JS (browser + Node.js) | Js |
| WASM/JS (browser + Node.js) | Js |
| iOS / macOS / tvOS / watchOS (Apple) | Darwin |
| Linux (x64, arm64) | Curl |
| Windows (mingwX64) | WinHttp |
// settings.gradle.kts
repositories {
maven("https://jitpack.io")
}
// build.gradle.kts
// Non-KMP Android/JVM app:
dependencies {
implementation("com.github.Luna712.NiceHttp-kmp:library-android:TAG")
}
// KMP project:
dependencies {
implementation("com.github.Luna712.NiceHttp-kmp:library:TAG")
}import com.lagradost.nicehttp.Requests
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
val app = Requests()
// GET
val response = app.get("https://httpbin.org/get") {
headers = mapOf("X-Custom" to "header")
params = mapOf("page" to "2")
timeout = 30.seconds
cacheTime = 1.days
verify = false
}
println(response.code) // 200
println(response.text) // body as String
println(response.ksoupDocument) // parsed as ksoup Document (all platforms)
println(response.document) // org.jsoup.Document on JVM/Android, ksoup on others
// POST with JSON
app.post("https://httpbin.org/post") {
json = JsonAsString("""{"name":"Alice"}""")
timeout = 10.seconds
}
// POST with form data
app.post("https://httpbin.org/post") {
data = mapOf("key" to "value")
}
// POST with typed JSON via SerializationResponseParser
@Serializable data class Payload(val name: String)
app.post("https://httpbin.org/post") {
json = Payload("Alice")
}
// Typed JSON response parsing
@Serializable data class Bin(val origin: String)
val bin = app.get("https://httpbin.org/get").parsed<Bin>()
println(bin.origin)All original call sites work unchanged:
import java.util.concurrent.TimeUnit
import okhttp3.Interceptor
// Original API still works on JVM/Android
app.get(
url,
headers = mapOf("X-Custom" to "header"),
cacheTime = 1,
cacheUnit = TimeUnit.DAYS,
timeout = 30L,
interceptor = myOkHttpInterceptor,
)
app.post(
url,
requestBody = "{}".toRequestBody("application/json".toMediaType()),
interceptor = myOkHttpInterceptor,
)import com.lagradost.nicehttp.Session
val session = Session()
session.post("https://example.com/login") {
data = mapOf("user" to "alice", "pass" to "s3cr3t")
}
val profile = session.get("https://example.com/profile") // cookies sent automatically// Retry failed requests
app.addInterceptor(RetryInterceptor(maxRetries = 3))
// Add headers to every request
app.addInterceptor(HeadersInterceptor(mapOf("Authorization" to "Bearer $token")))
// Log requests and responses
app.addInterceptor(LoggingInterceptor())
// Fallback to a different URL on failure
app.addInterceptor(FallbackUrlInterceptor(primaryUrl, fallbackUrl))
// Per-request interceptor
app.get(url) {
interceptor = LoggingInterceptor()
}class MyInterceptor : Interceptor {
override suspend fun intercept(ctx: HttpSendInterceptorContext): HttpClientCall {
ctx.request.headers.append("X-Custom", "value")
val call = ctx.proceed()
println("Response: ${call.response.status.value}")
return call
}
}These should be considered deprecated and will eventually be removed.
// Convert okhttp3.Interceptor to KMP Interceptor (may not always work for complex interceptors)
val kmpInterceptor = myOkHttpInterceptor.toNiceInterceptor()
// Convert KMP Interceptor to okhttp3.Interceptor
val okHttpInterceptor = myKmpInterceptor.toOkHttpInterceptor()app.post("https://httpbin.org/post") {
files = listOf(
NiceFile(name = "photo", fileName = "cat.png", bytes = imageBytes, fileType = "image/png"),
NiceFile(name = "caption", value = "My cat"),
)
}val response = app.get(url)
response.code // Int - HTTP status code
response.text // Deprecated; use text()
response.textLarge // Deprecated; use textLarge()
response.text() // String - body (cached, guarded at 5MB)
response.textLarge() // String - body without size guard
response.body // Deprecated; use body()
response.body().bytes() // ByteArray
response.body().string() // String
response.body().contentLength() // Long - content length in bytes
response.body().byteStream() // InputStream on JVM/Android, custom compatible implementation on other platforms
response.body().source() // okio.BufferedSource
response.cookies // Map<String, String>
response.headers // NiceHeaders - ktor Headers but also includes .toMap(), .toMultiMap() for back-compat
response.header("Content-Type") // String? - convenience header access for back-compat
response.isSuccessful // Boolean - true if 2xx
response.url // String - final URL after redirects
// HTML parsing
response.document // org.jsoup.Document on JVM/Android, ksoup on others; deprecated, use document() for ksoup
response.documentLarge // same but without size guard; deprecated, use documentLarge() for ksoup
response.document() // always com.fleeksoft.ksoup.nodes.Document
response.documentLarge() // same but without size guard
// JSON parsing (requires SerializationResponseParser or a custom parser)
response.parsed<MyClass>()
response.parsedSafe<MyClass>()
response.parsedLarge<MyClass>()
response.parsedSafeLarge<MyClass>()val app = Requests(responseParser = SerializationResponseParser())
@Serializable data class Repo(val name: String, val stargazers_count: Int)
val repo = app.get("https://api.github.com/repos/Kotlin/kotlinx.coroutines")
.parsed<Repo>()Custom parser:
class GsonParser : ResponseParser {
private val gson = Gson()
override fun <T : Any> parse(text: String, kClass: KClass<T>): T =
gson.fromJson(text, kClass.java)
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? =
runCatching { gson.fromJson(text, kClass.java) }.getOrNull()
override fun writeValueAsString(obj: Any): String = gson.toJson(obj)
}import com.lagradost.nicehttp.addGenericDns
import io.ktor.client.engine.okhttp.*
import okhttp3.OkHttpClient
val okClient = OkHttpClient.Builder()
.addGenericDns(
url = "https://dns.cloudflare.com/dns-query",
ips = listOf("1.1.1.1", "1.0.0.1"),
)
.build()
val app = Requests(
baseClient = HttpClient(OkHttp) {
engine { preconfigured = okClient }
}
)// In Application.onCreate() or similar
app.initClient(context)
// With SSL bypass (debug only)
app.initClient(context, ignoreSSL = true)| Original | KMP version |
|---|---|
OkHttpClient directly |
HttpClient (Ktor) - engine chosen per platform |
java.util.concurrent.TimeUnit |
kotlin.time.Duration (new API) / NiceTimeUnit (compat) |
okhttp3.Interceptor |
KMP Interceptor (new API) / NiceInterceptorCompat (compat) |
okhttp3.RequestBody |
KMP RequestBody (new API) / NiceRequestBodyCompat (compat) |
Jsoup.parse() |
Ksoup.parse() on all platforms, Jsoup.parse() on JVM via .document |
response.text (lazy val) |
response.text (lazy val, same API) |
response.document |
org.jsoup.Document on JVM/Android, ksoup on others |
response.headers.toMap() |
response.headers.toMap() - no import needed via NiceHeaders |
body.byteStream() |
body.byteStream() - InputStream on JVM, PlatformInputStream on others |
body.source() |
body.source() - BufferedSource |
All original NiceHttp call sites work unchanged on JVM/Android via compat types:
NiceTimeUnit=java.util.concurrent.TimeUniton JVM,kotlin.time.DurationUniton othersNiceInterceptorCompat=okhttp3.Interceptoron JVM, KMPInterceptoron othersNiceRequestBodyCompat=okhttp3.RequestBodyon JVM, KMPRequestBodyon others
These will be removed in a future version once all call sites are migrated.