Geonode logo
Geonode Team

Geonode Team

Actualizado: 16 de octubre de 2026

Publicado: 16 de octubre de 2026

Configuración del proxy de HttpClient en C#: credenciales, tiempos de espera y verificación

Esta guía explica cómo configurar un proxy HttpClient en C# con .NET 8: un `WebProxy` con `NetworkCredential`, la elección entre `HttpClientHandler` y `SocketsHttpHandler`, la reutilización correcta del cliente con `IHttpClientFactory`, los tiempos de espera y la cancelación, la configuración mediante variables de entorno, las listas de exclusión y las dos excepciones con las que te encontrarás realmente en producción. Termina con un programa de consola completo que abre una sesión residencial persistente, verifica la IP de salida y, en caso de fallo, vuelve a intentarlo con una nueva sesión. Todo lo que se muestra a continuación está orientado a .NET 8 con sentencias de nivel superior, pero la API se ha mantenido estable desde .NET Core 3.0 y el mismo código se compila en .NET 6 y 7.

Esta guía explica cómo configurar un proxy HttpClient en C# con .NET 8: un WebProxy con NetworkCredential, la elección entre HttpClientHandler y SocketsHttpHandler, la reutilización correcta del cliente con IHttpClientFactory, los tiempos de espera y la cancelación, la configuración mediante variables de entorno, las listas de exclusión y las dos excepciones con las que te encontrarás realmente en producción. Termina con un programa de consola completo que abre una sesión residencial persistente, verifica la IP de salida y, en caso de fallo, vuelve a intentarlo con una nueva sesión.

Todo lo que se muestra a continuación está orientado a .NET 8 con sentencias de nivel superior, pero la API se ha mantenido estable desde .NET Core 3.0 y el mismo código se compila en .NET 6 y 7.

WebProxy y NetworkCredential

El componente básico es System.Net.WebProxy, documentado en learn.microsoft.com. Contiene la dirección del proxy y, a través de su propiedad Credentials, el nombre de usuario y la contraseña que el controlador envía cuando el proxy solicita autenticación con el código de estado 407.

using System.Net;

var proxy = new WebProxy("http://premium-residential.geonode.com:9000")
{
    Credentials = new NetworkCredential("USERNAME", "PASSWORD")
};

El esquema de direcciones es http:// incluso cuando los destinos son HTTPS. Para una URL https://, el controlador envía una solicitud CONNECT al proxy y ejecuta TLS con el destino dentro de ese túnel, de modo que el salto del proxy en sí mismo se mantiene como HTTP sin cifrar y la validación del certificado se aplica al destino exactamente igual que lo haría sin un proxy.

Dado que las credenciales se encuentran en una NetworkCredential en lugar de en la URL, no es necesario codificar con el símbolo % los caracteres especiales de la contraseña. Esa es una ventaja práctica frente a los clientes que solo aceptan la información de usuario en la URL del proxy.

HttpClientHandler frente a SocketsHttpHandler

Hay dos clases de controladores que admiten un proxy. HttpClientHandler es la API tradicional; desde .NET Core 2.1, delega en SocketsHttpHandler en todas las plataformas, por lo que no hay ninguna diferencia funcional en cuanto a la compatibilidad con proxies. La razón para utilizar SocketsHttpHandler directamente son los controles adicionales que ofrece, y uno de ellos es de gran importancia para los proxies.

// Option A: HttpClientHandler
var handlerA = new HttpClientHandler
{
    Proxy = proxy,
    UseProxy = true
};

// Option B: SocketsHttpHandler (preferred for new code)
var handlerB = new SocketsHttpHandler
{
    Proxy = proxy,
    UseProxy = true,
    PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};

PooledConnectionLifetime es el parámetro. HttpClient agrupa las conexiones en un grupo y las reutiliza entre solicitudes. Con un proxy rotativo en el puerto 9000, la IP de salida se elige cuando se establece una conexión con el proxy; las solicitudes posteriores que utilicen la misma conexión agrupada pueden mantener la misma salida. Asignar a las conexiones agrupadas una duración limitada obliga a una reconexión periódica, lo que también permite detectar cambios en el DNS del host del proxy. Dos minutos es un valor predeterminado razonable; acórtalo si deseas que la rotación se produzca con más frecuencia de la que proporciona naturalmente la rotación de conexiones.

La tabla resume cuándo es adecuada cada opción.

NecesidadGestorConfiguración
Configuración mínima del proxy, código existenteHttpClientHandlerProxy, UseProxy = true
Reutilización limitada de conexiones con direcciones IP rotativasSocketsHttpHandlerPooledConnectionLifetime
Limitar las conexiones por host proxySocketsHttpHandlerMaxConnectionsPerServer
Desactivar el proxy para un clientecualquieraUseProxy = false

Reutilizar un HttpClient

HttpClient está diseñado para crearse una sola vez y reutilizarse durante todo el ciclo de vida de la aplicación. Crear una nueva instancia por cada solicitud agota los sockets bajo carga y, con un proxy, también desperdicia los túneles CONNECT del grupo que acabas de establecer con la latencia correspondiente. En una aplicación de consola o un worker, mantén una única instancia estática.

var http = new HttpClient(handlerB)
{
    Timeout = TimeSpan.FromSeconds(30)
};

IHttpClientFactory en ASP.NET Core

En ASP.NET Core, registra un cliente con nombre y adjunta el controlador mediante ConfigurePrimaryHttpMessageHandler. La fábrica gestiona el ciclo de vida del controlador por ti y rota los controladores según una programación, lo que evita tanto el problema del agotamiento de sockets como el DNS obsoleto.

// Program.cs
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("geonode", client =>
{
    client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var proxy = new WebProxy("http://premium-residential.geonode.com:9000")
    {
        Credentials = new NetworkCredential(
            Environment.GetEnvironmentVariable("GEONODE_USER"),
            Environment.GetEnvironmentVariable("GEONODE_PASS"))
    };

    return new SocketsHttpHandler
    {
        Proxy = proxy,
        UseProxy = true,
        PooledConnectionLifetime = TimeSpan.FromMinutes(2)
    };
});

var app = builder.Build();

app.MapGet("/ip", async (IHttpClientFactory factory) =>
{
    var client = factory.CreateClient("geonode");
    return await client.GetStringAsync("https://api.ipify.org?format=json");
});

app.Run();

Inyecta IHttpClientFactory dondequiera que necesites el cliente y llama a CreateClient("geonode"). El HttpClient devuelto es barato de crear porque el costoso manejador subyacente se comparte.

Tiempos de espera y CancellationToken

HttpClient.Timeout es el tiempo total permitido para una solicitud, incluyendo la conexión al proxy, el protocolo de enlace CONNECT, el TLS con el destino y la lectura de los encabezados de la respuesta. El valor por defecto es de 100 segundos, lo cual es demasiado tiempo para un proceso de rastreo. Configúralo en el cliente y, para un control más preciso, pasa un CancellationToken por cada solicitud.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));

try
{
    var body = await http.GetStringAsync("https://example.com/", cts.Token);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // HttpClient.Timeout elapsed
}
catch (TaskCanceledException)
{
    // the CancellationToken was cancelled (our 15 s budget)
}

Las salidas residenciales añaden latencia en comparación con una conexión directa, por lo que conviene prever unos segundos adicionales por solicitud. Un tiempo de espera constante en la misma fase en muchas solicitudes suele indicar un problema en la ruta de red hacia el servidor proxy, no que el destino sea lento.

DefaultProxy y variables de entorno

Si no se configura Proxy en el controlador, .NET utiliza HttpClient.DefaultProxy. En .NET Core 3.0 y versiones posteriores, esa propiedad estática se inicializa a partir de las variables de entorno HTTP_PROXY, HTTPS_PROXY y NO_PROXY, y la URL puede contener credenciales.

export HTTP_PROXY="http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
export HTTPS_PROXY="http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
export NO_PROXY="localhost,127.0.0.1,.internal"

También puedes asignar HttpClient.DefaultProxy en el código al inicio, lo que afecta a todos los controladores que no sobrescriban Proxy. Esto resulta útil en contenedores, pero se trata de un estado global: un cliente de comprobación de estado o una llamada a una API interna también pasarán por el proxy residencial, a menos que NO_PROXY incluya esos hosts en la lista. Para cualquier cosa que vaya más allá de un script desechable, resulta más claro especificar explícitamente WebProxy en un controlador concreto.

BypassList y BypassProxyOnLocal

WebProxy tiene su propio mecanismo de exclusión. BypassProxyOnLocal = true omite el proxy para los hosts cuyo nombre no contenga un punto (como localhost o un nombre de máquina sin adornos). BypassList admite expresiones regulares que coincidan con la URI de destino.

var proxy = new WebProxy("http://premium-residential.geonode.com:9000")
{
    Credentials = new NetworkCredential("USERNAME", "PASSWORD"),
    BypassProxyOnLocal = true,
    BypassList = new[] { @"^https?://(.*\.)?internal\.example(:\d+)?/" }
};

Recuerda que se trata de patrones de expresiones regulares, no de patrones glob; un punto sin escapar coincide con cualquier carácter.

Gestión del error 407 y otros errores

Hay dos tipos de error que representan casi todos los casos relacionados con los tickets de proxy de HttpClient. El primero es el error 407 «Proxy Authentication Required», que significa que el proxy rechazó las credenciales antes de reenviar nada. Normalmente aparece como HttpRequestException y, dependiendo de la versión de .NET y del punto de fallo, es posible que, en su lugar, se reciba una respuesta con el código de estado HttpStatusCode.ProxyAuthenticationRequired. Comprueba ambos.

using System.Net;
using System.Net.Http;

try
{
    using var response = await http.GetAsync("https://example.com/");

    if (response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
    {
        throw new InvalidOperationException("proxy rejected credentials (407)");
    }

    response.EnsureSuccessStatusCode();
    var html = await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
{
    // Also 407: fix the username grammar or password; retrying will not help.
}
catch (HttpRequestException ex)
{
    // Connection refused, DNS failure, TLS error, or EnsureSuccessStatusCode on 4xx/5xx.
    Console.Error.WriteLine($"request failed: {ex.Message} (status {ex.StatusCode})");
}

El segundo es el código TaskCanceledException, correspondiente a los tiempos de espera agotados, ya tratado anteriormente. La regla importante para ambos es la siguiente: un error 407 es un error de configuración y no debe reintentarse; un tiempo de espera agotado o un error 5xx del destino es transitorio y es un candidato razonable para un nuevo intento en una nueva sesión.

Sesiones persistentes mediante el nombre de usuario

El puerto 9000 asigna una nueva IP de salida para cada nueva conexión. Cuando un flujo necesita una misma IP para varias solicitudes (inicio de sesión seguido de páginas de datos o resultados paginados), utiliza el puerto 10000 y selecciona la sesión mediante el nombre de usuario. La gramática añade sufijos a tu nombre de usuario base: -country-xx para la segmentación geográfica, -session-XXXXXXXX con un identificador alfanumérico de 8 caracteres y, opcionalmente, -lifetime-SECONDS desde 180 hasta 86 400.

static string NewSessionId() =>
    Guid.NewGuid().ToString("N")[..8]; // 8 alphanumeric chars

static WebProxy StickyProxy(string baseUser, string pass, string country = "us", int lifetime = 600)
{
    var user = $"{baseUser}-country-{country}-session-{NewSessionId()}-lifetime-{lifetime}";
    return new WebProxy("http://premium-residential.geonode.com:10000")
    {
        Credentials = new NetworkCredential(user, pass)
    };
}

Cada solicitud realizada a través de un cliente creado en este proxy comparte la misma IP de salida durante diez minutos. Para cambiar de IP, crea un nuevo controlador con un nuevo identificador de sesión. Dado que la sesión está vinculada a las credenciales, dos clientes con el mismo nombre de usuario comparten la misma salida, incluso entre procesos.

Comprobación de la dirección IP de salida

Nunca des por hecho que se está utilizando el proxy de HttpClient; compruébalo. Realiza una solicitud https://api.ipify.org?format=json (o https://httpbin.org/ip) a través del cliente y compárala con tu dirección real. Con una sesión persistente, ambas llamadas devuelven la misma IP; con el puerto 9000 y una conexión nueva, las direcciones difieren.

using System.Text.Json;

var json = await http.GetStringAsync("https://api.ipify.org?format=json");
var ip = JsonDocument.Parse(json).RootElement.GetProperty("ip").GetString();
Console.WriteLine($"exit IP: {ip}");

Ejemplo completo en consola (.NET 8)

El programa que se muestra a continuación integra todos los elementos: una sesión persistente, tiempos de espera limitados, detección del código de error 407 y hasta tres intentos con una nueva sesión en caso de fallos transitorios. Créalo con dotnet new console -n ProxyDemo, sustituya Program.cs, configure GEONODE_USER y GEONODE_PASS, y ejecute dotnet run.

// Program.cs  (.NET 8, top-level statements)
using System.Net;
using System.Text.Json;

var baseUser = Environment.GetEnvironmentVariable("GEONODE_USER")
    ?? throw new InvalidOperationException("GEONODE_USER not set");
var password = Environment.GetEnvironmentVariable("GEONODE_PASS")
    ?? throw new InvalidOperationException("GEONODE_PASS not set");

var target = args.Length > 0 ? args[0] : "https://api.ipify.org?format=json";

for (var attempt = 1; attempt <= 3; attempt++)
{
    var sessionId = Guid.NewGuid().ToString("N")[..8];
    var user = $"{baseUser}-country-us-session-{sessionId}-lifetime-600";

    var handler = new SocketsHttpHandler
    {
        Proxy = new WebProxy("http://premium-residential.geonode.com:10000")
        {
            Credentials = new NetworkCredential(user, password)
        },
        UseProxy = true,
        PooledConnectionLifetime = TimeSpan.FromMinutes(2)
    };

    using var http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));

    try
    {
        // 1. Confirm the exit IP for this session.
        var ipJson = await http.GetStringAsync("https://api.ipify.org?format=json", cts.Token);
        var ip = JsonDocument.Parse(ipJson).RootElement.GetProperty("ip").GetString();
        Console.WriteLine($"[attempt {attempt}] session {sessionId} exit IP {ip}");

        // 2. Fetch the real target on the same IP.
        using var response = await http.GetAsync(target, cts.Token);

        if (response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
        {
            Console.Error.WriteLine("407: proxy rejected credentials, not retrying");
            return;
        }

        if ((int)response.StatusCode is 403 or 429 or >= 500)
        {
            Console.Error.WriteLine($"[attempt {attempt}] got {(int)response.StatusCode}, rotating session");
            continue;
        }

        var body = await response.Content.ReadAsStringAsync(cts.Token);
        Console.WriteLine($"{(int)response.StatusCode} {target} ({body.Length} chars)");
        return;
    }
    catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
    {
        Console.Error.WriteLine("407: proxy rejected credentials, not retrying");
        return;
    }
    catch (HttpRequestException ex)
    {
        Console.Error.WriteLine($"[attempt {attempt}] network error: {ex.Message}");
    }
    catch (TaskCanceledException)
    {
        Console.Error.WriteLine($"[attempt {attempt}] timed out");
    }

    await Task.Delay(TimeSpan.FromMilliseconds(500 * attempt));
}

Console.Error.WriteLine("gave up after 3 attempts");

El controlador se vuelve a crear deliberadamente en cada intento: una sesión persistente está vinculada al nombre de usuario de las credenciales, por lo que una nueva sesión requiere un nuevo WebProxy. En un servicio de larga duración, se almacenaría en caché un cliente por ID de sesión en lugar de por solicitud.

Errores comunes

Crear un HttpClient por cada solicitud

Cada nuevo cliente abre un nuevo túnel CONNECT y deja sockets en TIME_WAIT. Reutiliza una misma instancia o utiliza IHttpClientFactory, y deja que PooledConnectionLifetime se encargue de la rotación.

Uso de una dirección de proxy «https://»

El salto de proxy es HTTP sin cifrar; los destinos HTTPS viajan dentro del túnel CONNECT. Un esquema https:// en la dirección WebProxy indica a .NET que negocie TLS con el propio proxy, lo que falla ante un punto final que espera HTTP sin cifrar.

Reintentar un error 407

Un error 407 significa que el proxy no ha aceptado las credenciales. Reintentarlo con el mismo nombre de usuario y contraseña solo supone una pérdida de tiempo. Detecta el fallo rápidamente y comprueba la sintaxis del nombre de usuario, especialmente la longitud del identificador de sesión y el intervalo de vigencia.

Ignorar NO_PROXY y BypassList

Cuando el proxy procede de DefaultProxy, todas las llamadas salientes del proceso lo utilizan, incluidas las comprobaciones de estado y las API internas. Incluye esos hosts en NO_PROXY o BypassList, o configura el proxy únicamente para un cliente específico.

Siguiente paso

El código anterior funciona sin modificaciones con los proxies residenciales Geonode: 2,5 millones de direcciones IP en línea en más de 195 países, HTTP, HTTPS y SOCKS5 en un único host, y sesiones persistentes de hasta 24 horas seleccionadas mediante la sintaxis de nombre de usuario que se muestra aquí. La segmentación por país, estado, ciudad y ASN utiliza los mismos sufijos, por lo que restringir la ubicación de salida solo requiere un cambio en la cadena de texto, no en el código. La tarifa es la de pay-as-you-go sin contrato, a partir de 10 GB por 7,92 $ al mes, con una prueba de 3 días por 5 $. Consulta los proxies residenciales para obtener más detalles, o compara los planes en la página de precios. Para el resto del proceso, empieza por el caso de uso de web scraping.

Preguntas frecuentes

¿Cómo configuro un proxy HttpClient con nombre de usuario y contraseña en C#?

Crea un objeto WebProxy con la dirección del proxy y asigna a su propiedad Credentials un objeto NetworkCredential que contenga el nombre de usuario y la contraseña. Asigna ese WebProxy a la propiedad Proxy de un HttpClientHandler o un SocketsHttpHandler, establece UseProxy en true y pasa el controlador al constructor de HttpClient. El controlador envía las credenciales cuando el proxy responde con un 407.

¿Debería utilizar HttpClientHandler o SocketsHttpHandler para un proxy?

En .NET Core 2.1 y versiones posteriores, HttpClientHandler es una envoltura ligera sobre SocketsHttpHandler, por lo que ambos funcionan. SocketsHttpHandler expone PooledConnectionLifetime, lo que permite reciclar periódicamente las conexiones del grupo. Esto es importante con los proxies rotativos, ya que una conexión reutilizada mantiene la misma IP de salida hasta que se cierra. Es preferible utilizar SocketsHttpHandler para el código nuevo.

¿Por qué mi solicitud de proxy de HttpClient lanza una excepción TaskCanceledException?

HttpClient notifica un tiempo de espera agotado como TaskCanceledException, no como una excepción específica de tiempo de espera. Se lanza cuando expira HttpClient.Timeout o cuando se cancela el CancellationToken que has pasado. En .NET 5 y versiones posteriores, la excepción interna es una TimeoutException cuando la causa es el tiempo de espera del cliente, lo que permite distinguir entre ambas.

¿Lee HttpClient las variables de entorno HTTP_PROXY y HTTPS_PROXY?

Sí. En .NET Core 3.0 y versiones posteriores, HttpClient.DefaultProxy se inicializa a partir de HTTP_PROXY, HTTPS_PROXY y NO_PROXY, y los controladores lo utilizan cuando no se ha establecido ningún Proxy explícito. Las credenciales se pueden incluir en la URL. Configurar las variables de entorno resulta práctico en contenedores, pero es más fácil entender el código si se especifica explícitamente un valor WebProxy.

¿Cómo mantengo la misma IP en varias solicitudes de proxy de HttpClient?

Utiliza el puerto fijo 10000 y añade un sufijo de sesión al nombre de usuario, por ejemplo, USER-country-us-session-a1b2c3d4-lifetime-600. Todas las solicitudes enviadas con ese nombre de usuario comparten una misma IP de salida durante el tiempo de vida especificado en segundos, entre 180 y 86 400. Genera un nuevo identificador de sesión de 8 caracteres cuando desees una IP nueva.

¿Qué límites se aplican al realizar scraping a través de un proxy HttpClient residencial?

El ancho de banda se factura por gigabyte, así que descarga solo lo que necesites y evita las imágenes siempre que sea posible. Las sesiones fijas mantienen una IP durante un máximo de 24 horas y pueden finalizar antes si el dispositivo se desconecta, por lo que tu código debe gestionar un cambio de IP. Un proxy no te exime de cumplir las condiciones de servicio de un sitio web, las directivas para robots ni los límites de frecuencia.