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

Guía code-first para configurar proxies residenciales en Kotlin usando Ktor 3 y OkHttp, con ejemplos de geo-targeting, sesiones sticky, SOCKS5, concurrencia con coroutines y endurecimiento de producción.

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

Por qué usar proxies en Kotlin con Ktor y OkHttp

Si estás desarrollando un backend en Kotlin o una app Android que hace peticiones HTTP a APIs o sitios de terceros, tarde o temprano te enfrentarás a bloqueos por IP, rate limits agresivos o geo-restricciones. Usar proxies en Kotlin es la técnica que te permite enrutar tus peticiones a través de direcciones IP intermedias, evitando que tu servidor o dispositivo quede expuesto directamente al destino.

El problema existe porque muchos sitios —especialmente redes sociales, plataformas de e-commerce y motores de búsqueda— filtran por ASN. Si tu IP pertenece a un rango datacenter conocido (AWS, Google Cloud, DigitalOcean), te bloquean aunque no hayas hecho nada malicioso. Las proxies residenciales resuelven esto porque usan IPs asignadas a ISPs reales, lo que las hace indistinguibles del tráfico orgánico.

En esta guía verás cómo configurar Ktor Client 3 y OkHttp con ProxyHat, desde el proyecto base hasta patrones de producción con coroutines, control de concurrencia y manejo de desafíos 407.

Configuración del proyecto: Ktor 3 y OkHttp

Necesitas dos dependencias clave: el cliente HTTP de Ktor con el engine OkHttp (que internamente usa OkHttp como transporte) y OkHttp directamente para el baseline sin abstracciones. En un proyecto Gradle Kotlin DSL:

// build.gradle.kts
dependencies {
    // Ktor Client 3.x con engine OkHttp
    implementation("io.ktor:ktor-client-core: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")

    // OkHttp puro para el baseline
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    // Coroutines para concurrencia
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

El engine OkHttp de Ktor es ideal para Android y backends JVM porque hereda el pool de conexiones y el soporte HTTP/2 de OkHttp. Si necesitas máximo control sin capas intermedias, puedes usar OkHttp directamente.

Baseline: OkHttpClient con proxy explícito

Empecemos con el caso más simple: un OkHttpClient que enruta todo a través de ProxyHat sin autenticación en el constructor (la añadiremos después con un Authenticator):

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

fun createOkHttpBaseline(): OkHttpClient {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
    return OkHttpClient.Builder()
        .proxy(proxy)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()
}

fun main() {
    val client = createOkHttpBaseline()
    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

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

Esto funcionará para proxies sin auth, pero ProxyHat requiere credenciales. Cuando el proxy responde con 407 Proxy Authentication Required, OkHttp delega al Authenticator, que veremos en la sección de endurecimiento.

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

ProxyHat usa un gateway único: gate.proxyhat.com:8080 para HTTP y gate.proxyhat.com:1080 para SOCKS5. La autenticación y el control de geo-targeting van codificados en el nombre de usuario, no en parámetros de query.

Formatos de usuario soportados

EscenarioFormato de usernameEjemplo
Solo paísuser-country-{ISO}user-country-DE
País + ciudaduser-country-{ISO}-city-{name}user-country-DE-city-berlin
Sesión stickyuser-session-{id}user-session-abc123
Combinadouser-country-DE-session-abc123Geo + sesión persistente

Ktor Client con proxy auth en defaultRequest

En Ktor, la autenticación de proxy es engine-specific: el engine CIO no la soporta de forma nativa, pero el engine OkHttp sí, a través del header Proxy-Authorization. La forma más portable es añadirlo en defaultRequest:

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

fun createKtorClient(
    username: String = "user-country-DE-city-berlin",
    password: String = "pass"
): HttpClient {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
    val authHeader = "Basic " + Base64.getEncoder()
        .encodeToString("$username:$password".toByteArray())

    return HttpClient(OkHttp) {
        engine {
            this.proxy = proxy
        }
        defaultRequest {
            header("Proxy-Authorization", authHeader)
        }
    }
}

suspend fun main() {
    val client = createKtorClient()
    val response: String = client.get("https://httpbin.org/ip").bodyAsText()
    println(response)
    client.close()
}

Aquí el header Proxy-Authorization se envía en cada petición. Esto es necesario porque Ktor no expone una API directa para configurar credenciales de proxy en todos los engines. Si cambias al engine CIO, tendrás que manejarlo de otra forma o usar el engine OkHttp.

Sesiones sticky y rotación por petición

Para mantener la misma IP entre peticiones (útil para flujos de login o carritos de compra), añade -session-{id} al username. Para rotar en cada petición, simplemente cambia el session ID:

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.random.Random

suspend fun fetchWithRotatingIp(client: HttpClient, url: String): String {
    val sessionId = "sess-${Random.nextInt(0, 999999)}"
    // Recrear el cliente con un nuevo session ID o usar
    // un interceptor que actualice el header por petición
    return client.get(url) { /* el session ID va en el username del proxy */ }.bodyAsText()
}

En la práctica, si necesitas rotación por petición con Ktor, lo más limpio es crear un HttpClient por sesión o usar un interceptor que regenere el header Proxy-Authorization con un session ID aleatorio en cada llamada.

SOCKS5 en el puerto 1080

Para SOCKS5, ProxyHat expone el puerto 1080. Tanto OkHttp como Ktor (a través del engine OkHttp) soportan SOCKS5, pero la autenticación de SOCKS5 en la JVM se maneja con java.net.Authenticator.setDefault o con propiedades del sistema:

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

fun createSocks5Client(): OkHttpClient {
    // Configurar credenciales SOCKS5 vía system properties
    System.setProperty("java.net.socks.username", "user-country-US")
    System.setProperty("java.net.socks.password", "pass")

    val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080))
    return OkHttpClient.Builder()
        .proxy(proxy)
        .build()
}

fun main() {
    val client = createSocks5Client()
    val request = Request.Builder()
        .url("https://httpbin.org/ip")
        .build()

    client.newCall(request).execute().use { response ->
        println(response.body?.string())
    }
}

Esto es un patrón global: las properties java.net.socks.username y java.net.socks.password aplican a todas las conexiones SOCKS de la JVM. En un servidor con múltiples proxies SOCKS, esto es problemático. En ese caso, considera usar una librería como sockslib-java o encapsular cada proxy en un proceso separado.

Por qué necesitas proxies residenciales para Kotlin web scraping

Cuando haces Kotlin web scraping contra APIs de apps móviles o endpoints de redes sociales, los datacenter ASNs son bloqueados casi inmediatamente. Cloudflare y servicios como Cloudflare Bot Management mantienen listas de ASNs datacenter y los desafían con CAPTCHAs o los bloquean directamente.

Las proxies residenciales usan IPs asignadas por ISPs a usuarios finales. Esto significa que tu tráfico se ve como el de un usuario real conectado desde casa. Para scraping a escala, esto marca la diferencia entre un 95% de éxito y un 30% que se desploma tras los primeros 100 requests.

Ejemplo: fan-out con coroutines y Semaphore

Un patrón común es lanzar N peticiones concurrentes con rotación de IP, pero limitando la concurrencia para no saturar el gateway. Usamos async + awaitAll con un Semaphore para control de tasa:

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

suspend fun scrapeConcurrently(urls: List<String>, maxConcurrency: Int = 10) {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
    val semaphore = Semaphore(maxConcurrency)

    val client = HttpClient(OkHttp) {
        engine { this.proxy = proxy }
    }

    coroutineScope {
        val deferreds = urls.map { url ->
            async(Dispatchers.IO) {
                semaphore.withPermit {
                    try {
                        val sessionId = "sess-${(1..999999).random()}"
                        val auth = "Basic " + Base64.getEncoder()
                    .encodeToString("user-country-US-session-$sessionId:pass".toByteArray())

                        val response: HttpResponse = client.get(url) {
                            header("Proxy-Authorization", auth)
                            header("User-Agent", "Mozilla/5.0")
                        }
                        if (response.status.value in 200..299) {
                            Result.success(response.bodyAsText())
                        } else {
                            Result.failure(RuntimeException("HTTP ${response.status.value}"))
                        }
                    } catch (e: Exception) {
                        Result.failure(e)
                    }
                }
            }
        }

        val results = deferreds.awaitAll()
        results.forEachIndexed { i, result ->
            result.onSuccess { println("[$i] OK (${it.length} chars)") }
                .onFailure { println("[$i] FAIL: ${it.message}") }
        }
    }

    client.close()
}

suspend fun main() {
    val urls = (1..50).map { "https://httpbin.org/delay/${it % 3}" }
    scrapeConcurrently(urls, maxConcurrency = 10)
}

El Semaphore(10) garantiza que nunca haya más de 10 peticiones en vuelo simultáneamente. Cada petición usa un session ID único, lo que fuerza la rotación de IP en el gateway de ProxyHat. Con 50 URLs y 3 segundos de delay simulado, el tiempo total rondaría los 15 segundos en lugar de 150 si fueran secuenciales.

Endurecimiento de producción

Authenticator para 407 en OkHttp

Cuando el proxy devuelve 407 Proxy Authentication Required, OkHttp invoca al Authenticator registrado. Esto es más robusto que enviar el header manualmente porque maneja re-desafíos:

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

fun createProductionClient(): OkHttpClient {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))

    val proxyAuthenticator = Authenticator { route, response ->
        val credential = Credentials.basic("user-country-DE", "pass")
        response.request.newBuilder()
            .header("Proxy-Authorization", credential)
            .build()
    }

    return OkHttpClient.Builder()
        .proxy(proxy)
        .proxyAuthenticator(proxyAuthenticator)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES))
        .build()
}

Credentials.basic() genera exactamente el mismo header Proxy-Authorization: Basic ... que construimos manualmente en el ejemplo de Ktor. La ventaja es que OkHttp lo re-envía automáticamente si el proxy vuelve a desafiar.

Retries con backoff exponencial

Para Ktor, usa un interceptor o una función wrapper con backoff exponencial:

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.delay
import kotlin.math.min
import kotlin.math.pow

suspend fun <T> retryWithBackoff(
    maxRetries: Int = 3,
    baseDelayMs: Long = 500,
    maxDelayMs: Long = 10_000,
    block: suspend (attempt: Int) -> T
): T {
    var lastError: Exception? = null
    repeat(maxRetries) { attempt ->
        try {
            return block(attempt)
        } catch (e: Exception) {
            lastError = e
            if (attempt < maxRetries - 1) {
                val delayMs = min(maxDelayMs, (baseDelayMs * 2.0.pow(attempt)).toLong())
                delay(delayMs)
            }
        }
    }
    throw lastError ?: RuntimeException("Unknown error")
}

suspend fun safeFetch(client: HttpClient, url: String): String {
    return retryWithBackoff { attempt ->
        println("Attempt ${attempt + 1} for $url")
        client.get(url).bodyAsText()
    }
}

Configuración TLS y notas de Android

En Android 9+ (API 28+), el tráfico HTTP plano está bloqueado por defecto. Si tu app necesita conectar a un proxy HTTP (no HTTPS), debes añadir una NetworkSecurityConfig que permita el tráfico al dominio del proxy:

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">gate.proxyhat.com</domain>
    </domain-config>
</network-security-config>

Y referenciarlo en el AndroidManifest.xml:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

Esto solo permite cleartext hacia gate.proxyhat.com. El tráfico HTTPS hacia los destinos finales sigue usando TLS normalmente. Para producción, considera usar SOCKS5 en el puerto 1080, que no requiere cleartext HTTP hacia el proxy.

Consideraciones éticas y legales

El scraping de datos públicos es legal en muchos contextos, pero hay límites importantes:

  • CFAA (EE.UU.): La Computer Fraud and Abuse Act prohíbe el acceso no autorizado a sistemas protegidos. Si un sitio requiere login o tiene medidas técnicas de protección, saltárselas puede violar la CFAA.
  • GDPR (UE): Si recolectas datos personales de usuarios europeos, necesitas una base legal. Datos públicos de empresas suelen estar fuera del ámbito de protección de datos personales, pero los de individuos no.
  • robots.txt: Respeta las directivas de robots.txt del sitio destino. Aunque no es ley, es una señal de buena fe y algunos tribunales la han considerado relevante.
  • Términos de servicio: Leer y cumplir los ToS del sitio. Algunos prohíben explícitamente el scraping automatizado.

Prefiere siempre APIs oficiales cuando existan. Si una plataforma ofrece una API con rate limits razonables, úsala en lugar de scrapear. Es más estable, más legal y más mantenible. El scraping debe ser el último recurso, no el primero.

Para más detalles sobre casos de uso éticos, consulta nuestra guía de web scraping y SERP tracking.

Configuración específica de ProxyHat

ProxyHat expone un gateway unificado en gate.proxyhat.com. No necesitas endpoints diferentes por país o por tipo de proxy —todo se controla desde el username. Los docs de ProxyHat detallan todos los flags disponibles.

El patrón que vimos arriba (auth codificada en el username + header Proxy-Authorization) es exactamente el que usa el SDK de ProxyHat internamente. Si prefieres no manejar headers manualmente, el SDK abstrae la construcción del cliente. Los ejemplos de esta guía funcionan con cualquier plan de ProxyHat; consulta la página de precios para detalles de volumen y tipos de proxy disponibles (residencial, mobile y datacenter).

Para ver la cobertura geográfica completa, visita ubicaciones de ProxyHat, que incluye más de 195 países con geo-targeting a nivel de ciudad.

Key Takeaways

  • Usar proxies en Kotlin con Ktor requiere el engine OkHttp y el header Proxy-Authorization en defaultRequest, ya que la auth de proxy es engine-specific.
  • OkHttp ofrece proxyAuthenticator para manejar desafíos 407 de forma automática, más robusto que headers manuales.
  • El geo-targeting y las sesiones sticky se codifican en el username: user-country-DE-city-berlin-session-abc123.
  • SOCKS5 en el puerto 1080 usa java.net.socks.username/password como system properties, lo que es global en la JVM.
  • Para Kotlin web scraping a escala, usa Semaphore + async/awaitAll para controlar la concurrencia y evitar saturar el gateway.
  • En Android, configura NetworkSecurityConfig para permitir cleartext hacia gate.proxyhat.com si usas HTTP proxy en el puerto 8080.
  • Prefiere APIs oficiales sobre scraping. Respeta robots.txt, ToS y la legislación aplicable (CFAA, GDPR).

FAQ

¿Qué es usar proxies en Kotlin?

Es la práctica de enrutar peticiones HTTP/SOCKS desde aplicaciones Kotlin (backend o Android) a través de un servidor proxy intermedio, usando librerías como Ktor Client u OkHttp. El proxy recibe tu petición, la reenvía al destino con su propia IP y te devuelve la respuesta, ocultando tu IP original.

¿Por qué importa usar proxies en Kotlin para usuarios de proxy?

Porque Kotlin se usa tanto en backends como en Android, y ambos escenarios necesitan evadir bloqueos por IP, geo-restricciones y rate limits. Sin proxy, tu IP datacenter es bloqueada rápidamente por servicios como Cloudflare. Las proxies residenciales hacen que tu tráfico parezca orgánico.

¿Qué tipo de proxy funciona mejor para usar proxies en Kotlin?

Depende del caso: residenciales para scraping de sitios con anti-bot agresivo (redes sociales, e-commerce), datacenter para velocidad y APIs sin restricciones de ASN, y mobile para objetivos que filtran por tipo de conexión. ProxyHat ofrece los tres tipos en un mismo gateway.

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

Usa rotación de IP por petición (cambiando el session ID en el username), limita la concurrencia con Semaphore, añade delays aleatorios, respeta robots.txt, usa headers realistas (User-Agent, Accept) y implementa retries con backoff exponencial para manejar fallos transitorios.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog