Skip to content

Instantly share code, notes, and snippets.

@faruktoptas
Created May 17, 2026 16:05
Show Gist options
  • Select an option

  • Save faruktoptas/71bacb2e406c73171e3b746f66372d56 to your computer and use it in GitHub Desktop.

Select an option

Save faruktoptas/71bacb2e406c73171e3b746f66372d56 to your computer and use it in GitHub Desktop.
Ktor setup
sourceSets {
commonMain.dependencies {
val ktorVersion = "3.5.0" // Always check for the latest Ktor 3.x version
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
val httpClient = HttpClient(CIO) {
// 1. Base URL & Default Request Configurations
defaultRequest {
url("https://api.example.com/v1/")
header(HttpHeaders.ContentType, ContentType.Application.Json)
}
// 2. JSON Serialization / Deserialization
install(ContentNegotiation) {
json(Json {
...
})
}
// 3. Network Logging
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL // Options: NONE, HEADERS, BODY, ALL
}
// 4. Timeout Management
install(HttpTimeout) {
requestTimeoutMillis = 15000
connectTimeoutMillis = 15000
socketTimeoutMillis = 15000
}
// 5. Automatic Authentication (Bearer Token Interceptor)
install(Auth) {
bearer {
loadTokens {
// Fetch cached tokens from secure storage
val jwt = tokenHolder.getToken()
val refreshToken = tokenHolder.getRefreshToken()
if (token.isNotEmpty()){
BearerTokens(jwt, refreshToken)
}
}
refreshTokens {
// Triggered automatically whenever a 401 Unauthorized is received
val newTokens = fetchNewTokens()
BearerTokens(newTokens.accessToken, newTokens.refreshToken)
}
}
}
}
install(ContentNegotiation) {
json(Json {
// Ignore extra fields sent by the backend
ignoreUnknownKeys = true
// Accepts malformed JSON JSON (e.g. unquoted keys)
isLenient = true
// Formats the raw string printout for your Logging plugin
prettyPrint = true
// Fallback to default values defined in your data class if the key is missing
coerceInputValues = true
// Force sending default properties in POST/PUT requests
encodeDefaults = true
// Strict null payload checking for maximum type safety
explicitNulls = true
})
}
// data classes
@Serializable
data class User(val id: Int, val name: String, val email: String)
@Serializable
data class UserRequest(val name: String, val email: String)
// send request
suspend fun getUsers(): List<User> {
return httpClient.get("users").body()
}
// GET with Query Parameters
suspend fun searchUsers(query: String): List<User> {
return httpClient.get("users/search") {
parameter("query", query)
}.body()
}
// POST
suspend fun createUser(userRequest: UserRequest): User {
return httpClient.post("users") {
setBody(userRequest)
}.body()
}
// MULTIPART FORM DATA
import io.ktor.client.request.forms.*
suspend fun uploadProfilePicture(imageBytes: ByteArray) {
httpClient.post("user/avatar") {
setBody(MultiPartFormDataContent(
formData {
append("description", "Profile Picture")
append("image", imageBytes, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(HttpHeaders.ContentDisposition, "filename=\"avatar.jpg\"")
})
}
))
}
}
suspend fun safeApiCall() {
try {
val result = httpClient.get("protected-data").body<String>()
} catch (e: RedirectResponseException) { // 3xx Errors
println("Redirect Error: ${e.response.status.description}")
} catch (e: ClientRequestException) { // 4xx Errors (e.g., 404, 403)
println("Client Error: ${e.response.status.description}")
} catch (e: ServerResponseException) { // 5xx Errors (e.g., 500)
println("Server Error: ${e.response.status.description}")
} catch (e: Exception) { // No internet connection, timeout, etc.
println("An unexpected error occurred: ${e.message}")
}
}
val httpClient = HttpClient {
// Intercepting Outgoing Requests (Equivalent to a Request Interceptor)
sendPipeline.intercept(HttpSendPipeline.State) {
// 'context' represents the HttpRequestBuilder
val languageTag = "en-US" // This could come from a shared multiplatform settings library
context.header("X-App-Language", languageTag)
context.header("X-Platform", "KMP-Mobile")
proceed() // Continues the pipeline execution
}
// Intercepting Incoming Responses (Equivalent to a Response Interceptor)
receivePipeline.intercept(HttpReceivePipeline.State) { response ->
// Inspect headers, log specific events, or trigger local actions
if (response.status == HttpStatusCode.MaintenanceMode) {
// Trigger a global UI event to show a maintenance screen
}
proceed(response)
}
}
// retry
install(HttpRequestRetry) {
maxRetries = 3 // Retry up to 3 times
retryIf { request, response ->
response.status == HttpStatusCode.ServiceUnavailable // Only retry on 503 errors
}
delayMillis { retry ->
retry * 2000L // Exponential backoff: 2s, 4s, 6s...
}
modifyRequest { request ->
request.headers.append("X-Retry-Count", retryCount.toString())
}
}
// progress
// Tracking Download Progress
val response = client.get("https://example.com/large-asset.zip") {
onDownload { bytesSentTotal, contentLength ->
val progress = (bytesSentTotal.toDouble() / contentLength) * 100
println("Download progress: $progress%")
}
}
// Tracking Upload Progress
client.post("upload-video") {
setBody(videoByteArray)
onUpload { bytesSentTotal, contentLength ->
println("Uploaded: $bytesSentTotal bytes out of $contentLength")
}
}
// type safe resources
// Define your nested endpoint structure
@Resource("/users")
class UsersResource {
@Resource("{id}")
class Id(val parent: UsersResource = UsersResource(), val id: Int)
}
// Usage in your service (No strings required, zero risk of typos)
val user = client.get(UsersResource.Id(id = 42)).body<User>()
// unit testing
val mockEngine = MockEngine { request ->
respond(
content = """{"id":1,"name":"John Doe","email":"john@example.com"}""",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
val testClient = HttpClient(mockEngine) {
install(ContentNegotiation) { json() }
}
// Now use testClient inside your tests exactly like your real HttpClient!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment