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 proxy | Velocità tipica | Rilevabilità | Caso d'uso ideale |
|---|---|---|---|
| Datacenter | 10–50 ms | Alta | API pubbliche, target senza anti-bot |
| Residenziale | 100–500 ms | Bassa | SERP scraping, e-commerce, social |
| Mobile | 200–800 ms | Minima | App 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-Authorizationva indefaultRequest, non inconfig { }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 richiedeSystem.setPropertyper 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.txte 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-AuthorizationindefaultRequest, non nelconfig { }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.usernameejava.net.socks.passwordsono globali per JVM; per credenziali multiple serve unAuthenticatorcustom.- Sempre un Authenticator per il 407: OkHttp lo chiama automaticamente; previeni i loop con un contatore di
priorResponse.- Semaphore per rate limiting:
Semaphore(20)conwithPermitè 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.






