Lavorare con proxy HTTP in Kotlin è un'esigenza comune sia su backend JVM sia su Android. Che tu stia facendo Kotlin web scraping, monitoraggio prezzi o SERP tracking, devi gestire autenticazione proxy, rotazione IP, geo-targeting e resilienza ai blocchi. Questa guida copre l'uso di proxy in Kotlin con Ktor Client 3 (engine CIO/OkHttp) e OkHttp nativo, con esempi runnable e pattern di produzione.
Perché usare proxy residenziali in Kotlin
Molti target — app mobile, social network, marketplace — filtrano attivamente le richieste provenienti da ASN datacenter come AWS, Google Cloud o DigitalOcean. Uno studio di DataDome mostra che oltre il 50% del traffico automatizzato verso siti e-commerce viene bloccato prima di raggiungere l'applicazione. I proxy residenziali instradano le richieste attraverso IP assegnati a veri ISP, riducendo drasticamente la probabilità di ricevere un 403 o un challenge CAPTCHA.
Il problema tecnico è duplice:
- Il header
Proxy-Authorizationnon è gestito in modo uniforme da tutti i client HTTP: in Ktor CIO va aggiunto manualmente, in OkHttp esiste unAuthenticatordedicato. - La rotazione e il geo-targeting richiedono di codificare parametri nello username, un pattern non immediatamente ovvio.
Setup del progetto: Ktor 3 e OkHttp
Aggiungi le dipendenze in build.gradle.kts. Per un progetto backend JVM usiamo Ktor 3 con engine CIO; per Android o per chi preferisce OkHttp, includiamo anche l'engine OkHttp di Ktor e OkHttp nativo.
dependencies {
// Ktor Client 3.x core + engine CIO
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
// Engine OkHttp per Ktor (utile su Android)
implementation("io.ktor:ktor-client-okhttp:3.0.3")
// OkHttp nativo per uso diretto
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Coroutines per fan-out concorrente
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
Baseline OkHttp con proxy esplicito
Partiamo da un esempio minimo con OkHttpClient.Builder().proxy(java.net.Proxy(...)). Questo è il punto di riferimento prima di aggiungere autenticazione e rotazione.
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
fun main() {
val client = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url("https://httpbin.org/ip")
.build()
client.newCall(request).execute().use { response ->
println(response.code)
println(response.body?.string())
}
}
Senza credenziali riceverai un 407 Proxy Authentication Required. Vediamo come risolverlo.
Routing tramite gate.proxyhat.com:8080 con geo-targeting e sessioni sticky
ProxyHat usa il gateway gate.proxyhat.com:8080 per HTTP e :1080 per SOCKS5. Le credenziali includono flag di geo-targeting e sessione direttamente nello username, separati da trattini:
user-country-DE-region-state_of_berlin-city-berlin— IP residenziale a Berlino, Germania.user-country-US-sid-abc123— IP statunitense con sessione stickyabc123(lo stesso IP per richieste successive).user-country-IT-sid-ord7— IP italiano, sessione persistente.
Ktor Client: Proxy-Authorization in defaultRequest
In Ktor con engine CIO, l'autenticazione proxy non è gestita automaticamente dal motore: devi aggiungere l'header Proxy-Authorization: Basic ... tramite defaultRequest. Questo è un dettaglio engine-specific che causa molti mal di testa.
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.util.Base64
suspend fun main() {
val proxyUser = "user-country-DE-region-state_of_berlin-city-berlin"
val proxyPass = "pass"
val authHeader = "Basic " +
Base64.getEncoder().encodeToString("$proxyUser:$proxyPass".toByteArray())
val client = HttpClient(CIO) {
engine {
proxy = ProxyBuilder.http(
Url("http://gate.proxyhat.com:8080")
)
}
defaultRequest {
header("Proxy-Authorization", authHeader)
}
}
try {
val resp: HttpResponse = client.get("https://httpbin.org/ip")
println(resp.status)
println(resp.bodyAsText())
} finally {
client.close()
}
}
Per cambiare IP ad ogni richiesta basta variare lo username (es. aggiungendo un UUID come sessione). Per mantenere la stessa IP, riusa lo stesso identificativo di sessione.
OkHttp: Authenticator per sfide 407
OkHttp offre un percorso più elegante tramite Authenticator, che intercetta automaticamente le risposte 407 e aggiunge Proxy-Authorization.
import okhttp3.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
fun buildOkClient(user: String, pass: String): OkHttpClient {
val credential = Credentials.basic(user, pass)
return OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator { _: Route, response: Response ->
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
fun main() {
val client = buildOkClient("user-country-US-sid-s1", "pass")
val req = Request.Builder().url("https://httpbin.org/ip").build()
client.newCall(req).execute().use { r ->
println(r.code)
println(r.body?.string())
}
}
SOCKS5 sulla porta 1080
Per il protocollo SOCKS5, ProxyHat espone la porta 1080. Su JVM puoi impostare le proprietà di sistema socksProxyHost, socksProxyPort, java.net.socks.username e java.net.socks.password prima di creare il client. Questo funziona sia con Ktor CIO sia con OkHttp, poiché entrambi rispettano le proprietà globali della JVM.
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
suspend fun main() {
System.setProperty("socksProxyHost", "gate.proxyhat.com")
System.setProperty("socksProxyPort", "1080")
System.setProperty("java.net.socks.username", "user-country-FR-sid-sock9")
System.setProperty("java.net.socks.password", "pass")
val client = HttpClient(CIO)
try {
val resp = client.get("https://httpbin.org/ip")
println(resp.status)
println(resp.bodyAsText())
} finally {
client.close()
}
}
Nota: le proprietà di sistema sono globali. In un'app multi-tenant preferisci un'istanza java.net.Proxy con un java.net.Authenticator custom, o l'engine OkHttp di Ktor che supporta proxy SOCKS per-client.
Perché i residenziali servono per app e social: fan-out con coroutine
Target come Instagram, TikTok, Ticketmaster o sneaker shop bloccano rapidamente IP datacenter. I proxy residenziali di ProxyHat instradano tramite ISP reali, abbassando il tasso di blocco. Per sfruttare la concorrenza di Kotlin senza saturare il gateway, usiamo async/awaitAll con un Semaphore per il rate control.
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.*
import java.util.Base64
suspend fun fetchOne(client: HttpClient, authHeader: String, url: String): String {
return client.get(url) {
header("Proxy-Authorization", authHeader)
header(HttpHeaders.UserAgent, "Mozilla/5.0")
}.bodyAsText()
}
suspend fun main() = coroutineScope {
val urls = listOf(
"https://httpbin.org/ip?r=1",
"https://httpbin.org/ip?r=2",
"https://httpbin.org/ip?r=3",
"https://httpbin.org/ip?r=4",
"https://httpbin.org/ip?r=5",
"https://httpbin.org/ip?r=6",
"https://httpbin.org/ip?r=7",
"https://httpbin.org/ip?r=8"
)
val authHeader = "Basic " +
Base64.getEncoder().encodeToString("user-country-US:pass".toByteArray())
val client = HttpClient(CIO) {
engine {
proxy = ProxyBuilder.http(Url("http://gate.proxyhat.com:8080"))
}
}
val semaphore = kotlinx.coroutines.sync.Semaphore(4) // max 4 richieste concorrenti
try {
val results = urls.map { url ->
async(Dispatchers.IO) {
semaphore.withPermit {
runCatching { fetchOne(client, authHeader, url) }
.getOrElse { "ERR: ${it.message}" }
}
}
}.awaitAll()
results.forEachIndexed { i, r -> println("#$i: $r") }
} finally {
client.close()
}
}
Con Semaphore(4) limiti la concorrenza a 4 richieste in-flight, evitando di saturare il gateway e riducendo il rischio di rate limiting lato target. Per carichi maggiori, scala il numero di permessi e usa sessioni sticky distinte per shard logico (es. una sessione per categoria di prodotto).
Hardening di produzione
Retry, timeout e connection pooling in OkHttp
In produzione i timeout e i retry sono essenziali. OkHttp supporta retryOnConnectionFailure, Dispatcher per limitare richieste concorrenti e ConnectionPool per riutilizzare connessioni.
import okhttp3.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
fun buildProductionClient(user: String, pass: String): OkHttpClient {
val credential = Credentials.basic(user, pass)
return OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator { _, response ->
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.connectionPool(ConnectionPool(50, 5, TimeUnit.MINUTES))
.dispatcher(Dispatcher().apply {
maxRequests = 64
maxRequestsPerHost = 16
})
.build()
}
TLS e cipher
Per evitare fingerprinting TLS, usa il ConnectionSpec di OkHttp per forzare TLS 1.2/1.3 e cipher moderni. Su Android 7+ (API 24+) il NetworkSecurityConfig in res/xml/network_security_config.xml controlla trust anchor e cleartext; dichiaralo nel AndroidManifest.xml con android:networkSecurityConfig.
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">gate.proxyhat.com</domain>
</domain-config>
</network-security-config>
Il cleartext su gate.proxyhat.com è necessario perché il tunnel HTTP CONNECT verso il proxy avviene in chiaro prima del TLS end-to-end verso il target.
Circuit breaker con coroutine
Per evitare di bombardare un target già in errore, implementa un circuit breaker semplice con MutableStateFlow o usa una libreria come Resilience4j:
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class SimpleCircuitBreaker(
private val failureThreshold: Int = 5,
private val resetTimeoutMs: Long = 30_000L
) {
private val state = MutableStateFlow(State.CLOSED)
private var failures = 0
private var lastFailureTime = 0L
private val mutex = Mutex()
enum class State { CLOSED, OPEN, HALF_OPEN }
suspend fun <T> execute(block: suspend () -> T): T {
if (state.value == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > resetTimeoutMs) {
state.value = State.HALF_OPEN
} else {
throw IllegalStateException("Circuit open")
}
}
return try {
val result = block()
mutex.withLock { failures = 0; state.value = State.CLOSED }
result
} catch (e: Exception) {
mutex.withLock {
failures++
lastFailureTime = System.currentTimeMillis()
if (failures >= failureThreshold) state.value = State.OPEN
}
throw e
}
}
}
Considerazioni etiche e legali
Il web scraping è legale quando si raccolgono dati pubblici senza violare i termini di servizio o le normative sulla privacy. Negli Stati Uniti il Computer Fraud and Abuse Act (CFAA) disciplina l'accesso non autorizzato a sistemi informatici; nella sentenza hiQ Labs v. LinkedIn (2022) la corte ha confermato che lo scraping di dati pubblici non viola il CFAA. Nell'UE il GDPR si applica ai dati personali: se raccogli email, nomi o altri dati identificativi, devi avere una base giuridica.
Linee guida pratiche:
- Rispetta
robots.txte iRetry-Afterheader. - Limita la concorrenza per non impattare la disponibilità del target (usa il
Semaphoremostrato sopra). - Preferisci API ufficiali quando disponibili (es. API di Twitter/X, Serpstack, Google Custom Search).
- Non raccogliere dati personali senza base giuridica GDPR.
Per approfondire i casi d'uso, consulta le pagine web scraping e SERP tracking di ProxyHat, oppure la lista delle locazioni proxy disponibili.
Configurazione ProxyHat e SDK
ProxyHat espone un gateway unificato: HTTP su gate.proxyhat.com:8080, SOCKS5 su :1080. Lo username codifica geo-targeting e sessione. Il pattern è identico a quello mostrato negli esempi: cambia solo lo username per ruotare IP o cambiare paese. La documentazione ufficiale è su docs.proxyhat.com, e i piani sono visibili sulla pagina pricing.
Il ProxyHat SDK (quando disponibile per il tuo linguaggio) incapsula lo stesso pattern: costruttore di client con credenziali proxy, rotazione automatica delle sessioni e fallback tra pool residenziali. In Kotlin puoi replicarlo facilmente con una factory function che genera OkHttpClient o HttpClient parametrizzati per paese/sessione.
Punti chiave
- Ktor CIO richiede l'header
Proxy-Authorizationmanuale indefaultRequest; OkHttp usaproxyAuthenticator.- Codifica geo-targeting e sessioni sticky nello username:
user-country-DE-region-state_of_berlin-city-berlin-sid-abc123.- Usa
Semaphoreconasync/awaitAllper rate control nel fan-out concorrente.- Imposta timeout, connection pool e dispatcher per evitare saturazione del gateway.
- I proxy residenziali riducono i blocchi su target che filtrano ASN datacenter.
- Rispetta
robots.txt, CFAA e GDPR; preferisci API ufficiali quando possibili.






