Usar proxies en Kotlin con Ktor Client y OkHttp: guía práctica

Guía para desarrolladores Kotlin y Android sobre cómo enrutar tráfico HTTP y SOCKS5 a través de proxies residenciales con Ktor 3 y OkHttp, incluyendo geo-segmentación, sesiones pegajosas, concurrencia con corrutinas y endurecimiento para producción.

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp
En este artículo

Cuando intentas recoger datos públicos a escala desde una app Android o un backend Kotlin, los servidores de destino bloquean rápidamente los rangos de IP de datacenter. Usar proxies en Kotlin con Ktor Client y OkHttp te permite rotar IPs residenciales, fijar una ubicación geográfica y mantener sesiones pegajosas sin reinventar la rueda. Esta guía es code-first: cada concepto va acompañado de código ejecutable que apunta al gateway gate.proxyhat.com en el puerto 8080 (HTTP) y 1080 (SOCKS5).

Por qué usar proxies en Kotlin con Ktor Client y OkHttp

El problema es simple: los firewalls de WAF como Cloudflare y Akamai mantienen listas de Autonomous System Numbers (ASN) asociadas a proveedores cloud (AWS, DigitalOcean, OVH). Una petición legítima desde tu servidor Kotlin en eu-central-1 comparte ASN con miles de bots, así que el destino devuelve un 403 o un desafío CAPTCHA antes de siquiera leer tu User-Agent. Los proxies residenciales salen desde IPs asignadas a ISPs reales (Vodafone, Comcast, Movistar), por lo que el tráfico se mezcla con el de usuarios humanos.

Kotlin ofrece dos clientes HTTP idiomáticos para resolver esto:

  • Ktor 3 Client — multiplataforma, basado en corrutinas, con motores intercambiables (CIO, OkHttp, Darwin, Js).
  • OkHttp 4+ — el estándar de facto en Android, con pooling de conexiones, interceptores y soporte nativo de java.net.Proxy.

Ambos clientes delegan la autenticación del proxy al motor subyacente, lo que significa que la configuración es engine-specific. Más abajo verás cómo manejar el header Proxy-Authorization en cada uno.

Configuración del proyecto

Dependencias Gradle (Kotlin JVM)

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    application
}

dependencies {
    implementation("io.ktor:ktor-client-core:3.0.3")
    implementation("io.ktor:ktor-client-cio:3.0.3")
    implementation("io.ktor:ktor-client-okhttp:3.0.3")
    implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

Cliente Ktor 3 con motor CIO

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import java.net.InetSocketAddress
import java.net.Proxy

fun ktorCioClient(proxyHost: String = "gate.proxyhat.com", proxyPort: Int = 8080): HttpClient =
    HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort))
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 30_000
            connectTimeoutMillis = 10_000
        }
    }

Cliente OkHttp puro como baseline

import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

fun okHttpBaseline(): OkHttpClient = OkHttpClient.Builder()
    .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

fun main() {
    val client = okHttpBaseline()
    val req = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()
    client.newCall(req).execute().use { resp ->
        println(resp.body?.string())
    }
}

El motor CIO de Ktor no soporta Proxy-Authorization de forma automática; hay que inyectar el header a mano. OkHttp, en cambio, expone un Authenticator que se invoca tras un 407. Ambos patrones aparecen más abajo.

Enrutando a través de gate.proxyhat.com:8080

ProxyHat codifica geo-segmentación y sesiones pegajosas dentro del nombre de usuario, usando un formato de flags separadas por guiones. Esto evita parámetros extra en la URL y funciona con cualquier cliente HTTP estándar.

<
FlagEjemploSignificado
country-XXuser-country-DESalida desde Alemania
city-nameuser-country-DE-city-berlinSalida desde Berlín
session-xxxuser-session-abc123Sesión pegajosa: misma IP mientras dure el token

Proxy-Authorization en Ktor con defaultRequest

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

fun ktorProxyhatClient(user: String, pass: String, country: String? = null, session: String? = null): HttpClient {
    val username = buildString {
        append(user)
        if (country != null) append("-country-$country")
        if (session != null) append("-session-$session")
    }
    val token = Base64.getEncoder().encodeToString("$username:$pass".toByteArray())

    return HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
        }
        defaultRequest {
            headers.append("Proxy-Authorization", "Basic $token")
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 30_000
            connectTimeoutMillis = 10_000
        }
    }
}

fun main() = runBlocking {
    val client = ktorProxyhatClient("user", "pass", country = "DE", session = "abc123")
    val resp: HttpResponse = client.get("https://httpbin.org/ip")
    println(resp.bodyAsText())
    client.close()
}

El motor OkHttp de Ktor 3 hereda el Authenticator de OkHttp, por lo que si usas HttpClient(OkHttp) puedes omitir el defaultRequest y dejar que OkHttp resuelva el 407. Para CIO, el header manual es obligatorio.

OkHttp con Authenticator para 407

import okhttp3.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

fun okHttpProxyhat(user: String, pass: String, country: String? = null, session: String? = null): OkHttpClient {
    val username = buildString {
        append(user)
        if (country != null) append("-country-$country")
        if (session != null) append("-session-$session")
    }
    val creds = Credentials.basic(username, pass)

    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .proxyAuthenticator { _: Route?, response: Response ->
            response.request.newBuilder()
                .header("Proxy-Authorization", creds)
                .build()
        }
        .connectionPool(ConnectionPool(50, 5, TimeUnit.MINUTES))
        .build()
}

SOCKS5 en el puerto 1080

Para SOCKS5, Ktor y OkHttp delegan en java.net.Socket, que a su vez lee las credenciales de las propiedades del sistema java.net.socks.username y java.net.socks.password. Esto es global, así que si necesitas múltiples usuarios SOCKS5 en paralelo, aisla cada cliente en su propio ClassLoader o usa un proceso separado.

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import java.net.InetSocketAddress
import java.net.Proxy

fun socks5Client(): HttpClient = HttpClient(CIO) {
    engine {
        proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080))
    }
}

fun main() = runBlocking {
    System.setProperty("java.net.socks.username", "user-country-DE-city-berlin")
    System.setProperty("java.net.socks.password", "pass")
    val client = socks5Client()
    println(client.get("https://httpbin.org/ip").bodyAsText())
    client.close()
}

SOCKS5 es útil cuando el destino bloquea peticiones HTTP CONNECT explícitas o cuando necesitas tunelizar protocolos no HTTP (por ejemplo, WebSockets crudos sobre TLS). En la mayoría de casos de web scraping, HTTP en el 8080 es suficiente y más fácil de depurar.

Por qué los proxies residenciales son necesarios para scraping

Sitios de redes sociales y marketplaces (Instagram, TikTok, Amazon, Ticketmaster) bloquean por ASN. Una IP de OVH SAS (ASN 16276) que haga 50 peticiones por minuto a un endpoint de producto recibe un 429 o un bloqueo de Cloudflare en menos de 5 minutos. Una IP residencial de Movistar (ASN 3352) puede hacer las mismas 50 peticiones si respeta Retry-After y varía el fingerprint del navegador.

Según la documentación de Cloudflare sobre rangos IP, el filtrado por ASN es una de las primeras líneas de defensa anti-bot. Y el RFC 7231 define el código 429 como la señal estándar para backoff. Tu cliente Kotlin debería respetar ambos.

Fan-out con corrutinas y Semaphore

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
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

suspend fun fetchOne(client: HttpClient, url: String, sem: Semaphore): String =
    sem.withPermit {
        try {
            client.get(url).bodyAsText()
        } catch (e: Exception) {
            "ERROR: ${e.message}"
        }
    }

fun main() = runBlocking {
    val token = Base64.getEncoder().encodeToString("user-country-DE:pass".toByteArray())
    val client = HttpClient(CIO) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
        }
        defaultRequest { headers.append("Proxy-Authorization", "Basic $token") }
        install(HttpTimeout) { requestTimeoutMillis = 20_000 }
    }

    val urls = (1..200).map { "https://httpbin.org/anything?i=$it" }
    val sem = Semaphore(20)  // 20 concurrentes

    val results = urls.map { url ->
        async(Dispatchers.IO) { fetchOne(client, url, sem) }
    }.awaitAll()

    val ok = results.count { !it.startsWith("ERROR") }
    println("Éxito: $ok / ${urls.size}")
    client.close()
}

El Semaphore(20) limita la concurrencia a 20 peticiones en vuelo, lo que evita saturar el gateway y reduce los 429. Para 200 URLs con un 99.5% de éxito esperado, esto se completa en unos 30 segundos con latencias medias de 200ms por petición.

Endurecimiento para producción

Reintentos con backoff exponencial

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

suspend fun <T> retryWithBackoff(
    maxRetries: Int = 3,
    baseMs: Long = 500,
    block: suspend (attempt: Int) -> T
): T {
    var lastError: Throwable? = null
    repeat(maxRetries) { attempt ->
        try {
            return block(attempt)
        } catch (e: Exception) {
            lastError = e
            val jitter = Random.nextLong(0, 200)
            delay(min(baseMs * (1L shl attempt), 8_000L) + jitter)
        }
    }
    throw lastError ?: IllegalStateException("retry failed")
}

Configuración TLS para OkHttp

import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import java.security.KeyStore

fun okHttpTls(): OkHttpClient {
    val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
    tmf.init(KeyStore.getInstance(KeyStore.getDefaultType()))
    val tm = tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
    val sslContext = SSLContext.getInstance("TLSv1.3")
    sslContext.init(null, arrayOf(tm), null)

    return OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
        .sslSocketFactory(sslContext.socketFactory, tm)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .build()
}
</code></p>

<h3>Notas sobre Android NetworkSecurityConfig</h3>

<p>En Android 9+ (API 28), el tráfico en claro está bloqueado por defecto. Para depurar proxies HTTP sin TLS entre tu app y el gateway, añade un <code>network_security_config.xml</code> que permita el dominio del proxy solo en builds de debug:</p>

<pre><code><?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">gate.proxyhat.com</domain>
    </domain-config>
</network-security-config>

Y referéncialo en el <application> del AndroidManifest.xml con android:networkSecurityConfig="@xml/network_security_config". En producción, deja el tráfico cifrado y elimina esta excepción.

El SDK de ProxyHat y el mismo patrón

El SDK de ProxyHat encapsula exactamente este patrón: construye el usuario con flags, genera el Basic token y lo inyecta en cada petición. Si prefieres no gestionar manualmente el header, el SDK ofrece un cliente que rotar sesiones automáticamente y reintentar 407. Consulta la página de precios para ver los planes de rotación residencial y la lista de ubicaciones soportadas por país y ciudad.

Para casos de uso concretos, revisa las guías de web scraping y SERP tracking, donde se profundiza en cómo combinar rotación por petición con sesiones pegajosas para mantener carritos de compra o sesiones de login.

Errores comunes y casos límite

  • Olvidar Proxy-Authorization en CIO: el motor no lo añade solo; recibirás un 407 infinito.
  • Mezclar sesiones pegajosas con rotación global: si usas -session-abc123 en unas peticiones y no en otras, el gateway puede devolver IPs distintas para el mismo dominio y romper cookies.
  • Concurrencia sin Semaphore: lanzar 500 async sin límite agota los hilos del Dispatcher y dispara timeouts.
  • Ignorar Retry-After: aunque tu proxy rote IPs, el destino puede seguir devolviendo 429 si el fingerprint del cliente es idéntico.
  • Propiedades del sistema globales para SOCKS5: no funcionan para múltiples credenciales simultáneas.

Consideraciones éticas y legales

Scrapear datos públicos no es ilegal por sí mismo, pero el contexto importa. En EE.UU., la Computer Fraud and Abuse Act (CFAA) penaliza el acceso no autorizado a sistemas protegidos; los tribunales han interpretado que saltarse un bloqueo técnico explícito puede contar como "exceeding authorized access". En la UE, el GDPR protege los datos personales: recoger perfiles de usuarios de redes sociales sin base legal puede ser una infracción.

Buenas prácticas:

  • Respeta robots.txt y los Terms of Service del destino.
  • Prefiere APIs oficiales (Twitter API, Amazon Product Advertising API) cuando existan.
  • No recopiles datos personales sin consentimiento.
  • Limita la tasa por debajo de lo que un humano razonable haría.

Conclusiones clave

Key Takeaways:

  • Ktor CIO requiere Proxy-Authorization manual en defaultRequest; OkHttp lo maneja vía proxyAuthenticator.
  • Codifica geo-segmentación y sesiones en el usuario: user-country-DE-city-berlin-session-abc123.
  • SOCKS5 en el puerto 1080 usa propiedades del sistema globales; no es seguro para multi-usuario concurrente.
  • Limita la concurrencia con Semaphore y aplica backoff exponencial para respetar 429.
  • Los proxies residenciales son necesarios cuando el destino bloquea ASNs de datacenter.
  • Respeta robots.txt, ToS, CFAA y GDPR; prefiere APIs oficiales cuando sea posible.

Preguntas frecuentes

¿Qué es usar proxies en Kotlin?

Es la práctica de enrutar peticiones HTTP o SOCKS5 desde una app Kotlin (Android o backend) a través de un servidor proxy intermedio, usando clientes como Ktor o OkHttp. El proxy oculta tu IP de origen y, en el caso de proxies residenciales, presenta una IP de ISP real al destino.

¿Por qué importa usar proxies en Kotlin para scraping?

Porque los sitios bloquean por ASN. Sin un proxy residencial, tu backend en AWS o DigitalOcean comparte ASN con miles de bots y recibe 403/429. Un proxy residencial mezcla tu tráfico con el de usuarios humanos, aumentando la tasa de éxito de 30% a más de 95% en destinos protegidos por WAF.

¿Qué tipo de proxy funciona mejor para Kotlin?

Para scraping a escala, proxies residenciales con rotación por petición o sesiones pegajosas. Los datacenter son más rápidos (latencia 50-100ms) pero se bloquean fácilmente. Los móviles son ideales para apps sociales pero más caros. Usa HTTP en el puerto 8080 por defecto y SOCKS5 en 1080 solo si necesitas protocolos no HTTP.

¿Cómo evitas bloqueos al usar proxies en Kotlin?

Combina rotación de IP, sesiones pegajosas para mantener cookies, Semaphore para limitar concurrencia a 10-20 peticiones simultáneas, backoff exponencial ante 429, y rotación de User-Agent y headers. Nunca lances más de 50 peticiones por minuto desde una sola sesión sin respetar Retry-After.

¿Cómo se configura la autenticación de proxy en Ktor CIO?

Ktor CIO no añade Proxy-Authorization automáticamente. Debes codificar usuario:contraseña en Base64 y añadir el header Proxy-Authorization: Basic <token> dentro de defaultRequest. El motor OkHttp de Ktor sí lo gestiona vía Authenticator nativo.

Preguntas frecuentes

¿Qué es usar proxies en Kotlin?

Es la práctica de enrutar peticiones HTTP o SOCKS5 desde una app Kotlin (Android o backend) a través de un servidor proxy intermedio, usando clientes como Ktor o OkHttp. El proxy oculta tu IP de origen y, en el caso de proxies residenciales, presenta una IP de ISP real al destino.

¿Por qué importa usar proxies en Kotlin para scraping?

Porque los sitios bloquean por ASN. Sin un proxy residencial, tu backend en AWS o DigitalOcean comparte ASN con miles de bots y recibe 403/429. Un proxy residencial mezcla tu tráfico con el de usuarios humanos, aumentando la tasa de éxito de 30% a más de 95% en destinos protegidos por WAF.

¿Qué tipo de proxy funciona mejor para Kotlin?

Para scraping a escala, proxies residenciales con rotación por petición o sesiones pegajosas. Los datacenter son más rápidos (latencia 50-100ms) pero se bloquean fácilmente. Los móviles son ideales para apps sociales pero más caros. Usa HTTP en el puerto 8080 por defecto y SOCKS5 en 1080 solo si necesitas protocolos no HTTP.

¿Cómo evitas bloqueos al usar proxies en Kotlin?

Combina rotación de IP, sesiones pegajosas para mantener cookies, Semaphore para limitar concurrencia a 10-20 peticiones simultáneas, backoff exponencial ante 429, y rotación de User-Agent y headers. Nunca lances más de 50 peticiones por minuto desde una sola sesión sin respetar Retry-After.

¿Cómo se configura la autenticación de proxy en Ktor CIO?

Ktor CIO no añade Proxy-Authorization automáticamente. Debes codificar usuario:contraseña en Base64 y añadir el header Proxy-Authorization: Basic dentro de defaultRequest. El motor OkHttp de Ktor sí lo gestiona vía Authenticator nativo.

¿Listo para empezar?

Proxies residenciales, ISP y móviles en más de 148 países. Crea una cuenta gratis.

Crear cuenta gratis
← Volver al Blog