Conocimiento

Cómo usar proxies residenciales en Java con OkHttp y Apache HttpClient

Proxies en Java: el gotcha del proxyAuthenticator de OkHttp, el límite de pool por-ruta de 5 por defecto de Apache HttpClient, el consumo de entities, y rotación geo por petición.

Chris Collins

Chris Collins

25 de julio de 2026 · 10 min de lectura

Java es un caballo de batalla para la recolección de datos a escala: clientes HTTP maduros, hilos reales, y tooling de la JVM que hace observables los crawlers de larga duración. Conectar un proxy residencial a cualquiera de los dos clientes que la mayoría de los equipos usa, OkHttp y Apache HttpClient, es sencillo. La fricción está en los detalles que cada librería maneja de forma distinta: cómo se suministra la autenticación del proxy, un default del pool de conexiones que te limita en silencio, y la regla sobre consumir las respuestas que decide si el pooling funciona siquiera.

Esta es la entrada de Java de la misma serie que proxies residenciales con Python, con Playwright, y con Go: el código que funciona para ambos clientes, más las trampas específicas de Java.

Todo lo de abajo usa el gateway residencial de Shifter: un endpoint, p.shifter.io:443, con todo el targeting codificado en el nombre de usuario. Cambia host y credenciales para otro proveedor; la forma es la misma.

El modelo del gateway en un párrafo

El nombre de usuario del proxy lleva tu autenticación y tu targeting. No cambias de endpoint para cambiar de país o sesión, cambias la cadena del nombre de usuario:

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us apunta a EE. UU., sid fija una sesión sticky, ttl mantiene esa IP durante N segundos. Omite sid/ttl y cada nueva conexión rota. La contraseña es constante. Una arruga que vale la pena señalar de entrada: en ambos clientes de Java, las credenciales del proxy no se ponen en la URL del proxy, van a través de un mecanismo de autenticación dedicado. Eso es lo que más comúnmente se equivoca la gente.

OkHttp

OkHttp toma un objeto Proxy para la dirección y un proxyAuthenticator separado para las credenciales. No intentes codificar user:pass@ en una URL; OkHttp no lo leerá.

import okhttp3.*;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.io.IOException;
public class ProxyExample {
public static void main(String[] args) throws IOException {
String user = System.getenv("SHIFTER_USER") + "-country-us";
String pass = System.getenv("SHIFTER_PASS");
OkHttpClient client = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("p.shifter.io", 443)))
.proxyAuthenticator((route, response) -> {
// Llamado cuando el proxy devuelve 407. Adjunta Proxy-Authorization.
String credential = Credentials.basic(user, pass);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
})
.build();
Request request = new Request.Builder().url("https://api.ipify.org").build();
try (Response response = client.newCall(request).execute()) { // try-with-resources cierra el body
System.out.println(response.body().string()); // una IP residencial de EE. UU.
}
}
}

Dos cosas que interiorizar. La cadena user incluye los flags de targeting (-country-us), porque ahí es donde vive la geo. Y el try-with-resources alrededor del Response no es estilo opcional, cierra el body de la respuesta, que es lo que devuelve la conexión al pool.

Reutiliza el cliente. OkHttpClient está diseñado para crearse una vez y compartirse; mantiene el pool de conexiones y el dispatcher de hilos, y es thread-safe. Crear uno por petición tira el pooling y filtra recursos. Para variar la identidad por petición, deriva una variante con newBuilder(), que comparte el pool y el dispatcher subyacentes:

// Un cliente base, compartido. Las variantes por identidad reutilizan su pool + dispatcher.
OkHttpClient forGeo(OkHttpClient base, String country, String sid) {
String user = System.getenv("SHIFTER_USER") + "-country-" + country
+ (sid != null ? "-sid-" + sid + "-ttl-600" : "");
String pass = System.getenv("SHIFTER_PASS");
return base.newBuilder()
.proxyAuthenticator((route, resp) -> resp.request().newBuilder()
.header("Proxy-Authorization", Credentials.basic(user, pass))
.build())
.build();
}

Dale a cada unidad lógica de trabajo su propio sid y rota entre unidades, no a mitad de flujo (sticky vs rotating cubre la distinción).

Apache HttpClient (5.x)

Apache HttpClient suministra el proxy a través del request config o un route planner, y las credenciales a través de un CredentialsProvider con alcance al host del proxy.

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.*;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.auth.*;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.util.Timeout;
public class ApacheProxyExample {
public static void main(String[] args) throws Exception {
HttpHost proxy = new HttpHost("http", "p.shifter.io", 443);
String user = System.getenv("SHIFTER_USER") + "-country-us";
char[] pass = System.getenv("SHIFTER_PASS").toCharArray();
BasicCredentialsProvider creds = new BasicCredentialsProvider();
creds.setCredentials(new AuthScope(proxy),
new UsernamePasswordCredentials(user, pass));
// Pool: sube el por-ruta desde el default de 5 (ve el gotcha abajo).
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50);
RequestConfig config = RequestConfig.custom()
.setProxy(proxy)
.setConnectTimeout(Timeout.ofSeconds(10)) // fase de connect
.setResponseTimeout(Timeout.ofSeconds(30)) // fase de respuesta
.build();
try (CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultCredentialsProvider(creds)
.setDefaultRequestConfig(config)
.build()) {
HttpGet get = new HttpGet("https://api.ipify.org");
// try-with-resources en la respuesta consume + libera la conexión.
try (var response = client.execute(get)) {
System.out.println(new String(response.getEntity().getContent().readAllBytes()));
}
}
}
}

Nota los timeouts de connect y de respuesta separados, esa división connect-versus-response es exactamente lo que hace los timeouts diagnosticables cuando algo se cuelga.

Gotcha 1: el max-per-route por defecto de Apache es 5

Este es el equivalente en Java de la trampa de MaxIdleConnsPerHost de Go, y muerde fuerte. PoolingHttpClientConnectionManager tiene por defecto 2 conexiones por ruta y 20 en total en builds antiguos, y un cap por-ruta bajo en 5.x. Corre 50 hilos contra un host y la mayoría bloquean esperando a que una conexión se libere, lo cual parece exactamente un proxy lento.

Pon el pool al menos a tu concurrencia por host:

cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50); // >= tu concurrencia por host

Si el throughput de tu crawler se estanca sin importar cuántos hilos añadas, este default es lo primero que comprobar.

Gotcha 2: consume la entity, o la conexión nunca vuelve

Ambos clientes agrupan conexiones, y ambos solo devuelven una conexión al pool cuando la respuesta se consume por completo y se cierra. En Apache HttpClient, una entity sin consumir mantiene la conexión retirada; haz suficiente de eso y tu pool se muere de hambre aunque nada esté técnicamente filtrando.

Usa try-with-resources en la respuesta (como arriba), o consume explícitamente:

import org.apache.hc.core5.http.io.entity.EntityUtils;
// ...
EntityUtils.consume(response.getEntity()); // drena + libera la conexión

La trampa son las salidas tempranas: salir ante un status distinto de 200 sin consumir la entity deja varada la conexión. En un scraper que se topa con muchos bloqueos, eso es la mayor parte de tu tráfico, y el pool muere en silencio.

Gotcha 3: reutiliza el cliente, siempre

Tanto OkHttpClient como CloseableHttpClient son pesados, thread-safe, y están pensados para compartirse por toda la aplicación. Son dueños del pool de conexiones, y uno nuevo por petición paga un handshake TCP + TLS completo a través del proxy cada vez, el overhead que la guía de latencia existe para eliminar. Construye uno al arrancar, inyéctalo, y deriva variantes con alcance de petición solo cuando debas variar la identidad.

Rotar geo y sesiones

Como el targeting vive en el nombre de usuario, una identidad distinta es una cadena de credenciales distinta.

OkHttp: deriva un cliente con newBuilder() por identidad (mostrado arriba); reutiliza el pool compartido, así que sigue siendo eficiente.

Apache HttpClient: adjunta un HttpClientContext por petición que lleve un CredentialsProvider para esa identidad, para que un cliente sirva muchas identidades sin reconstruir:

HttpClientContext ctx = HttpClientContext.create();
BasicCredentialsProvider perCall = new BasicCredentialsProvider();
perCall.setCredentials(new AuthScope(proxy),
new UsernamePasswordCredentials(userFor("de", "job-42"), pass));
ctx.setCredentialsProvider(perCall);
client.execute(get, ctx, resp -> { /* maneja */ return null; });

Rota entre unidades lógicas de trabajo en lugar de dentro de una, y mapea trabajo a identidades como describe el post de balanceo de carga.

Concurrencia, por host

Acota la concurrencia por host objetivo, no con un cap global, para que un objetivo frágil no pueda ser martillado y uno permisivo no quede muerto de hambre. Un Semaphore por host es la expresión más simple:

Map<String, Semaphore> limits = Map.of(
"tough-site.example", new Semaphore(4),
"open-site.example", new Semaphore(32)
);
void fetch(String host, Runnable work) throws InterruptedException {
Semaphore sem = limits.get(host);
sem.acquire();
try { work.run(); } finally { sem.release(); }
}

Pasada la tolerancia de un objetivo, más hilos compran bloqueos, no throughput (cómo evitar que te bloqueen). Dimensiona el límite por-ruta del pool y tu semáforo por host juntos.

Verifica que de verdad estás en el proxy

Antes de hacer benchmark o depurar cualquier otra cosa, confirma la IP de salida:

Request req = new Request.Builder().url("http://ip-api.com/json").build();
try (Response r = client.newCall(req).execute()) {
System.out.println(r.body().string()); // espera una IP residencial en el país objetivo
}

Tu propia IP significa que el cliente no está usando el proxy. Un cuelgue significa que la salida local está bloqueada. Ambos están cubiertos en la guía de diagnóstico de timeouts.

Preguntas frecuentes

¿Por qué no funciona http://user:pass@host para el proxy en Java? Ni OkHttp ni Apache HttpClient leen credenciales de proxy en línea de una URL. OkHttp usa un proxyAuthenticator que pone Proxy-Authorization; Apache usa un CredentialsProvider con alcance al host del proxy. Como el gateway codifica el targeting en el nombre de usuario, ese nombre de usuario va en el authenticator/credentials, no en una URL.

Mi scraper de Java se estanca bajo carga incluso con un solo cliente. ¿Por qué? Lo más probable es que el límite por-ruta del pool de Apache (bajo por defecto) te esté limitando, o que no estés consumiendo las entities de respuesta así que las conexiones nunca vuelven al pool. Sube setDefaultMaxPerRoute a tu concurrencia y consume cada respuesta con try-with-resources.

¿Cómo roto IPs por petición en Java? Varía el nombre de usuario del proxy. En OkHttp, deriva un cliente por identidad con newBuilder() (comparte el pool). En Apache HttpClient, pasa un HttpClientContext por petición con un CredentialsProvider para esa identidad. Ambos dejan que un cliente compartido sirva muchas identidades.

¿Debería poner los timeouts de connect y de respuesta por separado? Sí. Un timeout de connect separado y un timeout de respuesta/socket te dejan distinguir un connect lento (tu lado o ninguna IP coincidente) de una respuesta lenta (el objetivo), que es la distinción clave al diagnosticar timeouts. Un único timeout romo esconde qué fase falló.

¿OkHttp o Apache HttpClient para scraping? Cualquiera funciona bien. OkHttp es más ligero y tiene una API más limpia; Apache HttpClient es más configurable y de larga trayectoria. Elige por ergonomía y dependencias existentes; la configuración del proxy y los gotchas de arriba aplican a ambos.

En resumen

Java más proxies residenciales es sólido una vez que respetas las reglas de cada cliente: suministra las credenciales del proxy a través del mecanismo apropiado (el proxyAuthenticator de OkHttp, el CredentialsProvider de Apache), nunca en línea en una URL; comparte un único cliente de larga vida y deriva variantes de identidad de él; sube el límite del pool por-ruta de Apache para que coincida con tu concurrencia; consume y cierra siempre las respuestas para que las conexiones vuelvan al pool; y separa los timeouts de connect y de respuesta. Varía el nombre de usuario del proxy para cambiar geo o sesión, y acota la concurrencia por host.

Acierta eso y ambos clientes rinden como la JVM debería. Apúntalos al gateway residencial, y recuerda que la calidad del pool decide con qué frecuencia estás reintentando siquiera (reputación de IP). La página de precios tiene los planes por GB para probarlo contra tus propios objetivos.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar