Usare Proxy in Kotlin con Ktor Client e OkHttp: Guida Pratica

Guida code-first per configurare proxy residenziali in Kotlin con Ktor Client e OkHttp: geo-targeting, sticky session, SOCKS5, autenticazione 407 e fan-out con coroutines.

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp

Se stai costruendo un scraper, un monitor di prezzi o un client API in Kotlin, prima o poi ti scontri con rate limiting, blocchi geografici o ban di ASN datacenter. Usare proxy in Kotlin non è difficile di per sé, ma la documentazione di Ktor e OkHttp lascia molti dettagli all'immaginazione: l'autenticazione del proxy è engine-specific, le sticky session vanno codificate nel username, e la gestione del 407 Proxy Authentication Required richiede un Authenticator personalizzato. Questa guida copre tutto, dalla configurazione di base al fan-out con coroutines in produzione.

Perché serve un proxy in Kotlin: il contesto tecnico

Quando fai una richiesta HTTP da un server Kotlin, l'IP sorgente appartiene all'ASN del tuo hosting provider. Molti target — social network, e-commerce, piattaforme di ticketing — filtrano attivamente gli ASN datacenter usando database come quelli di Autonomous System e servizi di IP reputation. Il risultato: HTTP 403, CAPTCHA interstitial, o ban silenziosi dopo 50–100 richieste.

I proxy residenziali risolvono questo problema instradando il traffico attraverso IP di dispositivi consumer reali, associati a ISP come Vodafone o Comcast. Questo rende il tuo traffico indistinguibile da quello di un utente legittimo. ProxyHat offre reti residenziali in oltre 150 paesi con geo-targeting a livello di città e rotazione IP per-request o sticky session.

Tipo proxyVelocità tipicaRilevabilitàCaso d'uso ideale
Datacenter10–50 msAltaAPI pubbliche, target senza anti-bot
Residenziale100–500 msBassaSERP scraping, e-commerce, social
Mobile200–800 msMinimaApp mobile, social media avanzati

Setup del progetto: dipendenze Gradle

Per iniziare, aggiungi Ktor Client 3.x con l'engine OkHttp e OkHttp nativo al tuo build.gradle.kts. L'engine OkHttp per Ktor è la scelta più flessibile perché dà accesso diretto al OkHttpClient.Builder per configurare proxy, TLS e connection pool.

// build.gradle.kts
val ktorVersion = "3.0.3"
val okhttpVersion = "4.12.0"

dependencies {
    // Ktor Client con engine OkHttp
    implementation("io.ktor:ktor-client-core:$ktorVersion")
    implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")

    // OkHttp nativo (per esempi raw senza Ktor)
    implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")

    // Coroutines per fan-out concorrente
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

Baseline: OkHttpClient raw con java.net.Proxy

Prima di passare a Ktor, vediamo il pattern base con OkHttp puro. Questo è il riferimento per capire cosa Ktor fa sotto il cofano.

import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
import java.time.Duration

fun rawOkHttpExample() {
    val proxyHost = "gate.proxyhat.com"
    val proxyPort = 8080
    val username = "user-country-DE-city-berlin"
    val password = "tua-password"

    val proxy = Proxy(
        Proxy.Type.HTTP,
        InetSocketAddress(proxyHost, proxyPort)
    )

    val authHeader = "Basic " + Base64.getEncoder()
        .encodeToString("$username:$password".toByteArray())

    val client = OkHttpClient.Builder()
        .proxy(proxy)
        .proxyAuthenticator { _, response ->
            response.request.newBuilder()
                .header("Proxy-Authorization", authHeader)
                .build()
        }
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(30))
        .build()

    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println("Status: ${response.code}")
        println("Body: ${response.body?.string()}")
    }
}

Due cose da notare: (1) il proxyAuthenticator intercetta il challenge 407 e reinietta l'header Proxy-Authorization; (2) il username codifica il geo-targeting (country-DE-city-berlin) direttamente nella stringa di autenticazione. ProxyHat usa questa convenzione per evitare parametri extra fuori banda.

Configurare Ktor Client con engine OkHttp e proxy

In Ktor 3, il proxy si configura a livello di engine. Con l'engine OkHttp, passi un blocco okHttp che espone il OkHttpClient.Builder. L'autenticazione del proxy, però, è un caso particolare: Ktor non gestisce Proxy-Authorization automaticamente nel layer HTTP, quindi devi aggiungerlo in defaultRequest.

import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

fun createKtorClient(
    country: String? = null,
    city: String? = null,
    sessionId: String? = null
): HttpClient {
    // Costruisci il username con flag geo e session
    val username = buildString {
        append("user")
        country?.let { append("-country-$it") }
        city?.let { append("-city-$it") }
        sessionId?.let { append("-session-$it") }
    }
    val password = "tua-password"

    val authHeader = "Basic " + Base64.getEncoder()
        .encodeToString("$username:$password".toByteArray())

    return HttpClient(OkHttp) {
        engine {
            // Configura il proxy HTTP a livello di engine
            config {
                proxy(Proxy(
                    Proxy.Type.HTTP,
                    InetSocketAddress("gate.proxyhat.com", 8080)
                ))
                // Connection pool per riuso TCP
                connectionPool(
                    okhttp3.ConnectionPool(
                        maxIdleConnections = 20,
                        keepAliveDuration = 5,
                        java.util.concurrent.TimeUnit.MINUTES
                    )
                )
            }
        }

        defaultRequest {
            // Proxy-Authorization va qui, non nel engine config
            header(HttpHeaders.ProxyAuthorization, authHeader)
            header(HttpHeaders.UserAgent, "Mozilla/5.0 (compatible; MyBot/1.0)")
        }
    }
}

suspend fun ktorProxyExample() {
    // Geo-targeting: Germania, Berlino + sticky session
    val client = createKtorClient(
        country = "DE",
        city = "berlin",
        sessionId = "abc123"
    )

    client.use { c ->
        val resp: HttpResponse = c.get("https://httpbin.org/ip")
        println("Status: ${resp.status.value}")
        println("Body: ${resp.bodyAsText()}")
    }
}

Nota critica: l'header Proxy-Authorization va in defaultRequest, non in config { } del engine. Ktor non ha un'API unificata per l'autenticazione proxy perché dipende dall'engine sottostante. Se usi l'engine CIO, il pattern è diverso e richiede System.setProperty per le credenziali HTTP.

SOCKS5 su porta 1080 con system properties

Per scenari dove HTTP CONNECT non è sufficiente — ad esempio tunneling verso endpoint che richiedono un transport layer diverso — ProxyHat supporta SOCKS5 sulla porta 1080. In Kotlin/Java, il modo più semplice è usare le system properties standard della JVM per l'autenticazione SOCKS.

import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy

defun socks5Example() {
    // Imposta credenziali SOCKS5 via system properties
    // (richiesto dal SOCKS5 implementation del JDK)
    System.setProperty("java.net.socks.username", "user-country-US")
    System.setProperty("java.net.socks.password", "tua-password")

    val proxy = Proxy(
        Proxy.Type.SOCKS,
        InetSocketAddress("gate.proxyhat.com", 1080)
    )

    val client = OkHttpClient.Builder()
        .proxy(proxy)
        .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .build()

    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println("SOCKS5 Status: ${response.code}")
        println("SOCKS5 IP: ${response.body?.string()}")
    }
}

Le system properties java.net.socks.username e java.net.socks.password sono lette dal SocksSocketImpl del JDK. Questo approccio funziona sia con OkHttp che con Ktor (engine CIO e OkHttp), ma ha un limite: le credenziali sono globali per la JVM. Se hai bisogno di credenziali SOCKS5 diverse per client diversi, devi implementare un java.net.Authenticator personalizzato che restituisce PasswordAuthentication in base al getRequestingHost().

Perché i proxy residenziali per app e social

I target social (Instagram, TikTok, Twitter/X) e le app mobile con API protette usano fingerprinting avanzato: oltre all'IP, controllano l'ASN, la coerenza del TLS fingerprint (JA3), e il pattern di richieste. Un IP datacenter di AWS o Hetzner viene flaggato quasi istantaneamente. I proxy residenziali di ProxyHat usano IP registrati a ISP consumer, quindi passano il primo livello di verifica ASN.

Secondo la documentazione di MDN sull'header Forwarded, i proxy trasparenti possono esporre l'IP originale tramite header come X-Forwarded-For. I proxy residenziali di ProxyHat sono non-transparent: non aggiungono header di forwarding, quindi l'IP sorgente visto dal target è quello del nodo residenziale.

Fan-out concorrente con coroutines e Semaphore

Il caso d'uso tipico: devi scrapare 500 URL concorrentemente, ma non vuoi saturare il pool di proxy o triggerare rate limit. La soluzione idiomatica in Kotlin è async + awaitAll con un Semaphore per controllare la concorrenza.

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
import io.ktor.http.*

suspend fun fanOutScrape(urls: List<String>, maxConcurrency: Int = 20) {
    val semaphore = Semaphore(maxConcurrency)

    val client = HttpClient(OkHttp) {
        engine {
            config {
                proxy(Proxy(
                    Proxy.Type.HTTP,
                    InetSocketAddress("gate.proxyhat.com", 8080)
                ))
            }
        }
        defaultRequest {
            val auth = "Basic " + Base64.getEncoder()
                .encodeToString("user-country-DE:tua-password".toByteArray())
            header(HttpHeaders.ProxyAuthorization, auth)
        }
    }

    client.use { c ->
        val results = coroutineScope {
            urls.map { url ->
                async(Dispatchers.IO) {
                    semaphore.withPermit {
                        try {
                            val resp: HttpResponse = c.get(url)
                            val status = resp.status.value
n                            val body = resp.bodyAsText()
                            Result.success(Pair(status, body.take(200)))
                        } catch (e: Exception) {
                            Result.failure<Pair<Int, String>>(e)
                        }
                    }
                }
            }.awaitAll()
        }

        var success = 0
        var failed = 0
        results.forEach { result ->
            result.fold(
                onSuccess = { (status, _) ->
                    if (status in 200..299) success++ else failed++
                    println("OK: $status")
                },
                onFailure = { e ->
                    failed++
                    println("ERR: ${e.message}")
                }
            )
        }
        println("Success: $success, Failed: $failed")
    }
}

// Esempio di chiamata
suspend fun main() {
    val urls = (1..500).map { "https://httpbin.org/anything?n=$it" }
    fanOutScrape(urls, maxConcurrency = 20)
}

Il Semaphore(20) garantisce al massimo 20 richieste in flight contemporaneamente. Con un proxy residenziale che ha una latenza di 100–500 ms per request, questo si traduce in circa 40–200 richieste al secondo di throughput effettivo. Aumenta maxConcurrency se il tuo target tollera più traffico, ma monitora sempre il success rate.

Production hardening: 407, retry, TLS e Android

Authenticator per 407 Proxy Authentication Required

In produzione, le credenziali proxy possono scadere o ruotare. OkHttp gestisce il 407 con un Authenticator che viene chiamato automaticamente. Implementa l'interfaccia e restituisci una nuova Request con l'header aggiornato.

import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy

class ProxyAuthenticator(
    private val username: String,
    private val password: String
) : Authenticator {

    private val credentials = Credentials.basic(username, password)

    override fun authenticate(route: Route?, response: Response): Request? {
        // Previeni loop infiniti: massimo 3 tentativi
        val attemptCount = response.priorResponse?.let {
            var count = 1
            var prior = it.priorResponse
            while (prior != null) {
                count++
                prior = prior.priorResponse
            }
            count
        } ?: 1

        if (attemptCount >= 3) return null

        return response.request.newBuilder()
            .header("Proxy-Authorization", credentials)
            .build()
    }
}

fun createHardenedClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .proxy(Proxy(
            Proxy.Type.HTTP,
            InetSocketAddress("gate.proxyhat.com", 8080)
        ))
        .proxyAuthenticator(
            ProxyAuthenticator("user-country-US-session-xyz789", "tua-password")
        )
        .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .build()
}

Retry con backoff esponenziale in Kotlin

import kotlinx.coroutines.*
import kotlin.math.min
import kotlin.random.Random

suspend fun <T> retryWithBackoff(
    maxRetries: Int = 3,
    baseDelayMs: Long = 1000,
    maxDelayMs: Long = 30_000,
    block: suspend (attempt: Int) -> T
): T {
    var lastException: Exception? = null

    for (attempt in 0 until maxRetries) {
        try {
            return block(attempt)
        } catch (e: Exception) {
            lastException = e
            if (attempt == maxRetries - 1) break

            val delay = min(
                (baseDelayMs * (2.0.pow(attempt))).toLong() +
                    Random.nextLong(0, 500),
                maxDelayMs
            )
            println("Retry ${attempt + 1}/$maxRetries after ${delay}ms: ${e.message}")
            delay(delay)
        }
    }
    throw lastException ?: RuntimeException("Retry exhausted")
}

// Uso con Ktor
suspend fun fetchWithRetry(client: HttpClient, url: String): String {
    return retryWithBackoff(maxRetries = 3) { attempt ->
        val resp = client.get(url)
        if (resp.status.value in 500..599) {
            throw RuntimeException("Server error: ${resp.status.value}")
        }
        resp.bodyAsText()
    }
}

TLS e Android NetworkSecurityConfig

Su Android 7+ (API 24+), la configurazione TLS di default blocca il cleartext traffic e richiede certificati validi. Se il tuo proxy usa un certificato self-signed per TLS interception (non è il caso di ProxyHat, che usa HTTPS end-to-end), devi aggiungere un network_security_config.xml. Per ProxyHat non serve: il tunnel HTTP CONNECT è cifrato end-to-end verso il target.

In OkHttp, puoi personalizzare la SSLContext e il ConnectionSpec se hai requisiti specifici:

import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import java.security.KeyStore

fun createTlsHardenedClient(): OkHttpClient {
    val tmf = TrustManagerFactory.getInstance(
        TrustManagerFactory.getDefaultAlgorithm()
    )
    tmf.init(null as KeyStore?)

    val sslContext = SSLContext.getInstance("TLSv1.3")
    sslContext.init(null, tmf.trustManagers, null)

    return OkHttpClient.Builder()
        .sslSocketFactory(
            sslContext.socketFactory,
            tmf.trustManagers[0] as javax.net.ssl.X509TrustManager
        )
        .connectionSpecs(
            listOf(
                ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                    .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
                    .build()
            )
        )
        .build()
}

Considerazioni etiche e legali

Lo scraping di dati pubblici è generalmente legale negli Stati Uniti sotto la giurisprudenza hiQ Labs v. LinkedIn (9th Circuit, 2022), che ha confermato che lo scraping di dati pubblicamente accessibili non viola il Computer Fraud and Abuse Act (CFAA). Tuttavia, i Termini di Servizio dei singoli platform possono vietare lo scraping, e la violazione dei ToS può avere conseguenze civilistiche.

Nell'UE, il GDPR si applica se raccogli dati personali (nome, email, IP). Anche se i dati sono pubblicamente accessibili, devi avere una base giuridica (legittimo interesse, consenso) e rispettare i diritti degli interessati. Consulta sempre le best practice di scraping etico e, quando possibile, preferisci API ufficiali.

Linee guida pratiche:

  • Rispetta robots.txt e i rate limit dichiarati.
  • Scrapa solo dati pubblicamente accessibili senza autenticazione.
  • Non memorizzare dati personali senza una base giuridica GDPR.
  • Se esiste un'API ufficiale, usala invece del scraping.
  • Per SERP tracking e price monitoring, usa proxy residenziali con rotazione per distribuire il carico.

ProxyHat SDK e integrazione

ProxyHat fornisce un gateway proxy standard alla gate.proxyhat.com:8080 (HTTP) e :1080 (SOCKS5). Non c'è un SDK Kotlin dedicato separato: il pattern mostrato sopra — credenziali codificate nel username, Proxy-Authorization header, e java.net.Proxy nel engine config — è l'integrazione ufficiale. Consulta la documentazione ufficiale di ProxyHat per i dettagli sui flag disponibili (country, city, session, ASN targeting).

Per il pricing e i piani disponibili, ProxyHat offre piani a consumo e illimitati con rotazione automatica. La configurazione è identica: cambia solo il username per cambiare paese, città o session.

Key Takeaways

  • Proxy auth è engine-specific: in Ktor 3 con engine OkHttp, aggiungi Proxy-Authorization in defaultRequest, non nel config { } del engine.
  • Geo-targeting e sticky session nel username: user-country-DE-city-berlin-session-abc123 è tutto quello che serve per controllare IP, posizione e persistenza della session.
  • SOCKS5 usa system properties: java.net.socks.username e java.net.socks.password sono globali per JVM; per credenziali multiple serve un Authenticator custom.
  • Sempre un Authenticator per il 407: OkHttp lo chiama automaticamente; previeni i loop con un contatore di priorResponse.
  • Semaphore per rate limiting: Semaphore(20) con withPermit è il pattern idiomatico Kotlin per controllare la concorrenza senza saturare il pool proxy.
  • Residenziale per social e app: i target che filtrano ASN datacenter richiedono IP residenziali; i datacenter vanno bene solo per API pubbliche.

FAQ

Come si usa un proxy in Kotlin con Ktor Client?

In Ktor 3 si configura il proxy a livello di engine: con l'engine OkHttp si imposta java.net.Proxy(Proxy.Type.HTTP, InetSocketAddress) nel OkHttpClient.Builder, e si aggiunge l'header Proxy-Authorization: Basic in defaultRequest perché l'autenticazione del proxy è engine-specific e non viene gestita automaticamente dal layer Ktor.

Quale tipo di proxy è meglio per il web scraping in Kotlin?

I proxy residenziali sono preferibili per target che bloccano gli ASN dei datacenter (social network, e-commerce, app mobile). I proxy datacenter offrono latenza più bassa (10–50 ms vs 100–500 ms) ma vengono rilevati più facilmente. I proxy mobile sono utili per simulare traffico da dispositivi mobili reali.

Come evitare blocchi usando proxy in Kotlin?

Usa rotazione IP con sticky session, geo-targeting preciso, rate limiting con Semaphore nelle coroutines, header User-Agent realistici, timeout e retry con backoff esponenziale, e rispetta robots.txt e i Termini di Servizio del sito target.

Come configurare SOCKS5 in Kotlin con OkHttp?

Per SOCKS5 si imposta java.net.Proxy(Proxy.Type.SOCKS, InetSocketAddress) nel OkHttpClient.Builder e si passano le credenziali tramite le system properties java.net.socks.username e java.net.socks.password, oppure si usa un java.net.Authenticator personalizzato.

Cos'è l'autenticazione proxy 407 e come gestirla in Kotlin?

Il codice 407 Proxy Authentication Required indica che il proxy richiede credenziali. In OkHttp si gestisce implementando l'interfaccia Authenticator e restituendo una Request con l'header Proxy-Authorization: Basic aggiornato. In Ktor si aggiunge l'header direttamente in defaultRequest.

Pronto per iniziare?

Accedi a oltre 50M di IP residenziali in oltre 148 paesi con filtraggio AI.

Vedi i prezziProxy residenziali
← Torna al Blog