Guía Completa de HTTP Proxy en C# y .NET 8: HttpClient, Rotación y TLS

Aprende a configurar HTTP proxies en .NET 8 con HttpClient, SocketsHttpHandler, Polly para reintentos, pools de proxies rotativos con DI, y configuración TLS avanzada. Incluye ejemplos de código listos para producción.

Guía Completa de HTTP Proxy en C# y .NET 8: HttpClient, Rotación y TLS

Si estás construyendo un scraper, un cliente API que necesita evitar rate limits, o simplemente quieres enrutar tráfico HTTP a través de proxies residenciales o datacenter en .NET 8+, este artículo es para ti. Configurar proxies en C# parece trivial hasta que te enfrentas a credenciales, rotación de IPs, manejo de errores y TLS. Vamos más allá del básico HttpClient.DefaultProxy y construimos soluciones robustas.

Por qué usar HTTP proxies en .NET

Los proxies HTTP interceptan tus solicitudes y las reenvían desde una IP diferente. En el contexto de scraping y automatización, esto es crucial para:

  • Evitar rate limits: Los sitios limitan solicitudes por IP. Rotar proxies distribuye la carga.
  • Geo-targeting: Acceder a contenido restringido por región usando IPs residenciales locales.
  • Anonimato: Ocultar tu IP origen en operaciones de investigación o auditoría.
  • Alta disponibilidad: Un pool de proxies te permite continuar si uno falla.

Los C# residential proxies son particularmente valiosos porque usan IPs de dispositivos reales, haciendo que tu tráfico parezca de usuarios legítimos en lugar de un datacenter conocido.

HttpClient con HttpClientHandler y WebProxy

El enfoque más directo en .NET 8 es configurar HttpClientHandler con un WebProxy. Este patrón funciona bien cuando usas un proxy fijo para todas las solicitudes.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public class BasicProxyExample
{
    private readonly HttpClient _httpClient;

    public BasicProxyExample()
    {
        // Configurar el proxy con credenciales
        var proxy = new WebProxy("http://gate.proxyhat.com:8080", BypassOnLocal: false)
        {
            Credentials = new NetworkCredential("user-country-US", "PASSWORD")
        };

        // Lista de URLs que bypass el proxy (opcional)
        proxy.BypassList = new string[]
        {
            "localhost",
            "127.0.0.1",
            "*.internal.company.com"
        };

        // Handler con el proxy configurado
        var handler = new HttpClientHandler
        {
            Proxy = proxy,
            UseProxy = true,
            UseDefaultCredentials = false,
            PreAuthenticate = true,
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 10
        };

        // HttpClient con timeout configurable
        _httpClient = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    public async Task<string> FetchAsync(string url)
    {
        try
        {
            var response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Error en solicitud: {ex.Message}");
            throw;
        }
    }
}

// Uso
var client = new BasicProxyExample();
var html = await client.FetchAsync("https://httpbin.org/ip");
Console.WriteLine(html);

Este patrón es simple pero tiene limitaciones: el proxy es estático, no hay rotación automática, y cada instancia de HttpClient está vinculada a un único proxy. Para scraping serio, necesitamos más flexibilidad.

SocketsHttpHandler con Proxy por Solicitud

En .NET 8, SocketsHttpHandler es el handler moderno y de alto rendimiento. A diferencia de HttpClientHandler, permite configurar el proxy por solicitud usando HttpRequestMessage, lo cual es ideal para rotación dinámica.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public class PerRequestProxyExample
{
    private readonly HttpClient _httpClient;

    public PerRequestProxyExample()
    {
        var handler = new SocketsHttpHandler
        {
            // Timeout para establecer conexión
            ConnectTimeout = TimeSpan.FromSeconds(15),
            
            // Tiempo de vida del pool de conexiones (importante para proxies rotativos)
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
            
            // Permitir redirecciones
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 10,
            
            // Configuración SSL
            SslOptions = new System.Net.Security.SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    // En producción, valida correctamente el certificado
                    return errors == System.Net.Security.SslPolicyErrors.None;
                }
            }
        };

        _httpClient = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    public async Task<string> FetchWithProxyAsync(
        string url, 
        string proxyUrl, 
        string username, 
        string password)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        
        // Configurar proxy para esta solicitud específica
        var proxy = new WebProxy(proxyUrl)
        {
            Credentials = new NetworkCredential(username, password)
        };
        
        request.Options.Set(
            new HttpRequestOptionsKey<IWebProxy>("WebProxy"), 
            proxy
        );

        try
        {
            using var response = await _httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error con proxy {proxyUrl}: {ex.Message}");
            throw;
        }
    }
}

// Uso con ProxyHat
var example = new PerRequestProxyExample();

// Solicitud 1: IP residencial de EE.UU.
var result1 = await example.FetchWithProxyAsync(
    "https://httpbin.org/ip",
    "http://gate.proxyhat.com:8080",
    "user-country-US",
    "PASSWORD"
);

// Solicitud 2: IP residencial de Alemania
var result2 = await example.FetchWithProxyAsync(
    "https://httpbin.org/ip",
    "http://gate.proxyhat.com:8080",
    "user-country-DE",
    "PASSWORD"
);

El parámetro PooledConnectionLifetime es crítico cuando rotas proxies: conexiones antiguas pueden apuntar a proxies que ya no están disponibles. Un timeout de 2 minutos asegura que el pool se refresque regularmente.

Polly para Reintentos y Circuit Breaker

Los proxies fallan. Rate limits, IPs bloqueadas, timeouts del servidor proxy. Polly es la biblioteca estándar en .NET para manejar fallos de forma resiliente con políticas de retry, circuit breaker y timeout.

using System;
using System.Net;
using System.Net.Http;
using Polly;
using Polly.Retry;
using Polly.CircuitBreaker;

public class ResilientProxyClient
{
    private readonly HttpClient _httpClient;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
    private readonly AsyncCircuitBreakerPolicy _circuitBreaker;

    public ResilientProxyClient()
    {
        var handler = new SocketsHttpHandler
        {
            ConnectTimeout = TimeSpan.FromSeconds(10),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };

        _httpClient = new HttpClient(handler);

        // Política de reintentos con backoff exponencial
        _retryPolicy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => (int)r.StatusCode >= 500 || r.StatusCode == HttpStatusCode.TooManyRequests)
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Reintento {retryCount} después de {timeSpan.TotalSeconds}s debido a: {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");
                }
            );

        // Circuit breaker: abre después de 5 fallos consecutivos
        _circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (ex, breakDuration) =>
                {
                    Console.WriteLine($"Circuit abierto por {breakDuration.TotalSeconds}s: {ex.Message}");
                },
                onReset: () =>
                {
                    Console.WriteLine("Circuit cerrado, solicitudes normales reanudadas");
                }
            );
    }

    public async Task<string> FetchWithRetryAsync(string url, string proxyUser, string proxyPass)
    {
        // Combinar políticas: retry + circuit breaker
        var strategy = Policy.WrapAsync(_retryPolicy, _circuitBreaker);

        var result = await strategy.ExecuteAsync(async () =>
        {
            var proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(proxyUser, proxyPass)
            };

            var request = new HttpRequestMessage(HttpMethod.Get, url);
            request.Options.Set(new HttpRequestOptionsKey<IWebProxy>("WebProxy"), proxy);

            var response = await _httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();
            return response;
        });

        return await result.Content.ReadAsStringAsync();
    }
}

// Uso
var client = new ResilientProxyClient();
var content = await client.FetchWithRetryAsync(
    "https://httpbin.org/ip",
    "user-country-US",
    "PASSWORD"
);

La combinación de retry con backoff exponencial y circuit breaker protege tanto tu aplicación como el servicio objetivo. Si un proxy específico está fallando consistentemente, el circuit breaker evita spampear solicitudes que sabes van a fallar.

Parallel.ForEachAsync para Scraping Concurrente

.NET 8 introduce Parallel.ForEachAsync, una forma moderna y eficiente de ejecutar operaciones asíncronas en paralelo con control de concurrencia. Es ideal para scraping masivo con proxies rotativos.

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public class ConcurrentScraper
{
    private readonly HttpClient _httpClient;

    public ConcurrentScraper()
    {
        var handler = new SocketsHttpHandler
        {
            ConnectTimeout = TimeSpan.FromSeconds(15),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            MaxConnectionsPerServer = 100 // Ajustar según capacidad
        };

        _httpClient = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    public async Task<Dictionary<string, string>> ScrapeUrlsAsync(
        List<string> urls, 
        List<(string User, string Pass)> proxyCredentials,
        int maxDegreeOfParallelism = 10)
    {
        var results = new ConcurrentDictionary<string, string>();
        var proxyIndex = 0;
        var proxyLock = new object();

        await Parallel.ForEachAsync(urls, new ParallelOptions
        {
            MaxDegreeOfParallelism = maxDegreeOfParallelism
        }, async (url, cancellationToken) =>
        {
            // Rotar proxies de forma thread-safe
            (string user, string pass) proxy;
            lock (proxyLock)
            {
                proxy = proxyCredentials[proxyIndex % proxyCredentials.Count];
                proxyIndex++;
            }

            try
            {
                var proxyObj = new WebProxy("http://gate.proxyhat.com:8080")
                {
                    Credentials = new NetworkCredential(proxy.user, proxy.pass)
                };

                var request = new HttpRequestMessage(HttpMethod.Get, url);
                request.Options.Set(new HttpRequestOptionsKey<IWebProxy>("WebProxy"), proxyObj);

                using var response = await _httpClient.SendAsync(request, cancellationToken);
                response.EnsureSuccessStatusCode();
                
                var content = await response.Content.ReadAsStringAsync(cancellationToken);
                results.TryAdd(url, content);
                
                Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] ✓ {url} - IP: {proxy.user}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] ✗ {url} - Error: {ex.Message}");
                results.TryAdd(url, $"ERROR: {ex.Message}");
            }
        });

        return results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }
}

// Uso: scraping de múltiples URLs con rotación de proxies
var scraper = new ConcurrentScraper();

var urls = Enumerable.Range(1, 50)
    .Select(i => $"https://httpbin.org/delay/1?id={i}")
    .ToList();

var proxies = new List<(string, string)>
{
    ("user-country-US", "PASSWORD"),
    ("user-country-DE", "PASSWORD"),
    ("user-country-GB", "PASSWORD"),
    ("user-country-JP", "PASSWORD"),
    ("user-country-BR", "PASSWORD")
};

var results = await scraper.ScrapeUrlsAsync(urls, proxies, maxDegreeOfParallelism: 10);
Console.WriteLine($"Completado: {results.Count} URLs procesadas");

Parallel.ForEachAsync maneja automáticamente la limitación de concurrencia y la cancelación. Ajusta MaxDegreeOfParallelism según la capacidad de tu pool de proxies y el rate limit del sitio objetivo.

Servicio de Pool de Proxies Rotativos con DI

En aplicaciones .NET 8 con inyección de dependencias, conviene encapsular la lógica de rotación de proxies en un servicio reutilizable. Este patrón permite cambiar estrategias de rotación sin modificar el código cliente.

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

// Configuración
public class ProxyPoolOptions
{
    public string GatewayHost { get; set; } = "gate.proxyhat.com";
    public int HttpPort { get; set; } = 8080;
    public string Username { get; set; } = "";
    public string Password { get; set; } = "";
    public List<string> Countries { get; set; } = new() { "US" };
    public bool RotatePerRequest { get; set; } = true;
    public int SessionDurationSeconds { get; set; } = 300;
    public int MaxRetries { get; set; } = 3;
}

// Interface
public interface IProxyPool
{
    Task<HttpClient> GetClientAsync();
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
    void RotateSession();
}

// Implementación
public class ProxyPoolService : IProxyPool, IDisposable
{
    private readonly ProxyPoolOptions _options;
    private readonly ILogger<ProxyPoolService> _logger;
    private readonly ConcurrentBag<HttpClient> _clientPool = new();
    private readonly Random _random = new();
    private int _sessionCounter = 0;
    private string _currentSession = "";
    private DateTime _sessionExpiry = DateTime.MinValue;
    private readonly object _sessionLock = new();

    public ProxyPoolService(
        IOptions<ProxyPoolOptions> options,
        ILogger<ProxyPoolService> logger)
    {
        _options = options.Value;
        _logger = logger;
        InitializeSession();
    }

    private void InitializeSession()
    {
        lock (_sessionLock)
        {
            _sessionCounter++;
            _currentSession = $"session{_sessionCounter}_{Guid.NewGuid():N}";
            _sessionExpiry = DateTime.UtcNow.AddSeconds(_options.SessionDurationSeconds);
            _logger.LogInformation("Nueva sesión proxy creada: {Session}", _currentSession);
        }
    }

    public void RotateSession()
    {
        InitializeSession();
    }

    private string GetProxyUsername()
    {
        lock (_sessionLock)
        {
            // Rotar sesión si expiró
            if (DateTime.UtcNow >= _sessionExpiry)
            {
                InitializeSession();
            }

            var country = _options.Countries[_random.Next(_options.Countries.Count)];
            
            // Formato ProxyHat: user-country-US-session-abc123
            return $"{_options.Username}-country-{country}-session-{_currentSession}";
        }
    }

    public async Task<HttpClient> GetClientAsync()
    {
        var proxyUsername = GetProxyUsername();
        
        var handler = new SocketsHttpHandler
        {
            ConnectTimeout = TimeSpan.FromSeconds(15),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            Proxy = new WebProxy($"http://{_options.GatewayHost}:{_options.HttpPort}")
            {
                Credentials = new NetworkCredential(proxyUsername, _options.Password)
            },
            UseProxy = true
        };

        var client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };

        _clientPool.Add(client);
        return client;
    }

    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
    {
        var proxyUsername = GetProxyUsername();
        
        // Crear proxy para esta solicitud específica
        var proxy = new WebProxy($"http://{_options.GatewayHost}:{_options.HttpPort}")
        {
            Credentials = new NetworkCredential(proxyUsername, _options.Password)
        };

        request.Options.Set(new HttpRequestOptionsKey<IWebProxy>("WebProxy"), proxy);

        using var handler = new SocketsHttpHandler
        {
            ConnectTimeout = TimeSpan.FromSeconds(15),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };

        using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };

        for (int attempt = 0; attempt < _options.MaxRetries; attempt++)
        {
            try
            {
                var response = await client.SendAsync(request);
                
                if (response.IsSuccessStatusCode)
                {
                    _logger.LogDebug("Solicitud exitosa a {Url} con proxy {Proxy}", 
                        request.RequestUri, proxyUsername);
                    return response;
                }

                if ((int)response.StatusCode >= 500)
                {
                    _logger.LogWarning("Reintento {Attempt} para {Url}: {Status}", 
                        attempt + 1, request.RequestUri, response.StatusCode);
                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
                    continue;
                }

                return response;
            }
            catch (HttpRequestException ex)
            {
                _logger.LogError(ex, "Error en intento {Attempt} para {Url}", 
                    attempt + 1, request.RequestUri);
                
                if (attempt == _options.MaxRetries - 1)
                    throw;
                    
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
            }
        }

        throw new HttpRequestException("Máximo número de reintentos alcanzado");
    }

    public void Dispose()
    {
        foreach (var client in _clientPool)
        {
            client.Dispose();
        }
    }
}

// Registro en Program.cs
// builder.Services.Configure<ProxyPoolOptions>(builder.Configuration.GetSection("ProxyPool"));
// builder.Services.AddSingleton<IProxyPool, ProxyPoolService>();

Este servicio integra automáticamente rotación de sesiones, selección de país, reintentos y logging estructurado. Se puede inyectar en cualquier servicio que necesite hacer solicitudes HTTP a través de proxies.

TLS Avanzado: Certificate Pinning y Custom Root CA

Cuando trabajas con proxies HTTPS, el proxy actúa como man-in-the-middle. Esto es normal para proxies residenciales y datacenter, pero requiere configuración TLS específica. En algunos casos, necesitas certificate pinning o confiar en una CA raíz personalizada.

using System;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

public class TlsProxyClient
{
    private readonly HttpClient _httpClient;
    private readonly X509Certificate2? _pinnedCertificate;
    private readonly X509Certificate2? _customRootCa;

    public TlsProxyClient(string? pinnedCertPath = null, string? customRootCaPath = null)
    {
        // Cargar certificado pinned si existe
        if (!string.IsNullOrEmpty(pinnedCertPath))
        {
            _pinnedCertificate = new X509Certificate2(pinnedCertPath);
        }

        // Cargar CA raíz personalizada
        if (!string.IsNullOrEmpty(customRootCaPath))
        {
            _customRootCa = new X509Certificate2(customRootCaPath);
        }

        var handler = new SocketsHttpHandler
        {
            ConnectTimeout = TimeSpan.FromSeconds(15),
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            SslOptions = new SslClientAuthenticationOptions
            {
                // Callback para validación personalizada de certificados
                RemoteCertificateValidationCallback = ValidateServerCertificate,
                
                // Opciones adicionales de TLS
                EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13,
                
                // Certificate revocation check
                CertificateRevocationCheckMode = X509RevocationCheckMode.NoCheck // Para proxies
            }
        };

        _httpClient = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(30)
        };
    }

    private bool ValidateServerCertificate(
        object sender,
        X509Certificate? certificate,
        X509Chain? chain,
        SslPolicyErrors sslPolicyErrors)
    {
        // Si hay errores de política SSL, evaluar si son aceptables
        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            return true;
        }

        // Para proxies MITM, es común tener errores de nombre o root no confiable
        if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
        {
            Console.WriteLine("Warning: Certificate name mismatch (común en proxies MITM)");
        }

        if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
        {
            Console.WriteLine("Warning: Certificate chain errors");
        }

        // Certificate pinning: verificar contra certificado conocido
        if (_pinnedCertificate != null && certificate != null)
        {
            var incomingCert = new X509Certificate2(certificate);
            
            // Comparar thumbprint (SHA-1 hash del certificado)
            if (string.Equals(
                incomingCert.Thumbprint, 
                _pinnedCertificate.Thumbprint, 
                StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("Certificate pinned: thumbprint match");
                return true;
            }
            
            Console.WriteLine($"Certificate pinning failed. Expected: {_pinnedCertificate.Thumbprint}, Got: {incomingCert.Thumbprint}");
            return false;
        }

        // Validar contra CA raíz personalizada
        if (_customRootCa != null && chain != null)
        {
            chain.ChainPolicy.ExtraStore.Add(_customRootCa);
            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
            
            if (chain.Build(new X509Certificate2(certificate!)))
            {
                Console.WriteLine("Certificate validated against custom root CA");
                return true;
            }
        }

        // Para scraping con proxies residenciales, aceptar certificados con advertencias
        // ADVERTENCIA: Solo usar en producción si entiendes los riesgos
        Console.WriteLine($"Accepting certificate with errors: {sslPolicyErrors}");
        return true;
    }

    public async Task<string> FetchAsync(string url, string proxyUser, string proxyPass)
    {
        var proxy = new WebProxy("http://gate.proxyhat.com:8080")
        {
            Credentials = new NetworkCredential(proxyUser, proxyPass)
        };

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Options.Set(new HttpRequestOptionsKey<IWebProxy>("WebProxy"), proxy);

        try
        {
            var response = await _httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"TLS Error: {ex.Message}");
            throw;
        }
    }

    // Método para obtener información del certificado del servidor
    public async Task<X509Certificate2?> GetServerCertificateAsync(string url)
    {
        X509Certificate2? serverCert = null;

        var handler = new SocketsHttpHandler
        {
            SslOptions = new SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    if (cert != null)
                    {
                        serverCert = new X509Certificate2(cert);
                    }
                    return true; // Aceptar para inspección
                }
            }
        };

        using var client = new HttpClient(handler);
        await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
        
        return serverCert;
    }
}

// Uso con certificate pinning
var tlsClient = new TlsProxyClient(
    pinnedCertPath: "./pinned-server-cert.pem",
    customRootCaPath: "./proxy-root-ca.pem"
);

var result = await tlsClient.FetchAsync(
    "https://example.com",
    "user-country-US",
    "PASSWORD"
);

Comparación de Enfoques

Cada patrón tiene ventajas y desventajas según tu caso de uso:

Enfoque Ventajas Desventajas Caso de uso ideal
HttpClientHandler + WebProxy Simple, proxy estático, fácil de configurar Sin rotación dinámica, proxy fijo por instancia Clientes API con proxy corporativo fijo
SocketsHttpHandler por solicitud Proxy dinámico, alto rendimiento, .NET 8 nativo Requiere gestión manual de conexiones Scraping con rotación de IPs
Polly + HttpClient Resiliencia automática, backoff exponencial Complejidad adicional, configuración de políticas Sistemas de producción críticos
Parallel.ForEachAsync Concurrencia controlada, escalabilidad Requiere pool de proxies amplio Scraping masivo de miles de URLs
Servicio DI con Pool Reutilizable, testeable, configuración centralizada Overhead inicial de configuración Aplicaciones empresariales .NET

Mejores Prácticas para C# HTTP Proxy

  • Reutiliza HttpClient: No crees nuevos HttpClient por solicitud. Usa IHttpClientFactory o instancias long-lived con handlers configurados.
  • Configura timeouts apropiados: Los proxies añaden latencia. Timeouts de 30-60 segundos son razonables para scraping.
  • Implementa logging estructurado: Registra proxy usado, tiempo de respuesta, y códigos de estado para debugging.
  • Monitorea el pool: Los proxies residenciales pueden desconectarse. Implementa health checks y rotación automática.
  • Respeta robots.txt: Incluso con proxies, el scraping ético reduce riesgo de bloqueos.
  • Usa sesiones sticky cuando sea necesario: Para flujos de login, usa el mismo proxy durante toda la sesión.

Consejo de ProxyHat: Para scraping de SERPs con C# residential proxies, combina sesiones sticky (para mantener estado) con rotación de país (para comparar resultados regionales). Nuestro formato de username user-country-US-session-abc123 te permite controlar ambos aspectos sin cambiar tu código.

Ejemplo Completo: Scraper SERP con DI

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

// Program.cs - Configuración completa con DI
var builder = Host.CreateDefaultBuilder(args);

builder.ConfigureServices((context, services) =>
{
    // Configuración del pool de proxies
    services.Configure<ProxyPoolOptions>(options =>
    {
        options.GatewayHost = "gate.proxyhat.com";
        options.HttpPort = 8080;
        options.Username = "your_username";
        options.Password = context.Configuration["PROXY_PASSWORD"];
        options.Countries = new List<string> { "US", "GB", "DE", "FR" };
        options.RotatePerRequest = true;
        options.SessionDurationSeconds = 300;
        options.MaxRetries = 3;
    });

    // Registrar el servicio de pool de proxies
    services.AddSingleton<IProxyPool, ProxyPoolService>();

    // Registrar Polly si no está incluido
    // services.Add Polly();

    // Servicio de scraping SERP
    services.AddScoped<ISerpScraper, SerpScraperService>();
});

var app = builder.Build();

// Ejecutar scraper
using var scope = app.Services.CreateScope();
var scraper = scope.ServiceProvider.GetRequiredService<ISerpScraper>();

var results = await scraper.ScrapeAsync("c# http proxy tutorial", new[] { "US", "GB" });

foreach (var result in results)
{
    Console.WriteLine($"{result.Country}: {result.OrganicResults.Count} resultados");
}

// Interface del scraper SERP
public interface ISerpScraper
{
    Task<List<SerpResult>> ScrapeAsync(string query, string[] countries);
}

public record SerpResult(string Country, List<OrganicResult> OrganicResults);
public record OrganicResult(int Position, string Title, string Url, string Snippet);

public class SerpScraperService : ISerpScraper
{
    private readonly IProxyPool _proxyPool;
    private readonly ILogger<SerpScraperService> _logger;

    public SerpScraperService(IProxyPool proxyPool, ILogger<SerpScraperService> logger)
    {
        _proxyPool = proxyPool;
        _logger = logger;
    }

    public async Task<List<SerpResult>> ScrapeAsync(string query, string[] countries)
    {
        var results = new List<SerpResult>();
        var encodedQuery = Uri.EscapeDataString(query);

        await Parallel.ForEachAsync(countries, async (country, ct) =>
        {
            var url = $"https://www.google.com/search?q={encodedQuery}&gl={country.ToLower()}&hl=en";
            
            using var request = new HttpRequestMessage(HttpMethod.Get, url);
            request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
            request.Headers.Add("Accept", "text/html");
            request.Headers.Add("Accept-Language", "en-US,en;q=0.9");

            try
            {
                var response = await _proxyPool.SendAsync(request);
                var html = await response.Content.ReadAsStringAsync(ct);
                
                var organicResults = ParseOrganicResults(html);
                results.Add(new SerpResult(country, organicResults));
                
                _logger.LogInformation("Scraped {Count} results for {Country}", 
                    organicResults.Count, country);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to scrape for {Country}", country);
            }
        });

        return results;
    }

    private List<OrganicResult> ParseOrganicResults(string html)
    {
        // Implementar parsing según estructura de Google SERP
        // Usar HtmlAgilityPack o similar
        var results = new List<OrganicResult>();
        
        // Parsing simplificado (implementar con HTML parser real)
        // ...
        
        return results;
    }
}

Key Takeaways

  • HttpClientHandler es simple pero limitado: Úsalo para proxies estáticos. Para rotación dinámica, prefiere SocketsHttpHandler con proxy por solicitud.
  • PooledConnectionLifetime importa: Configura tiempos de vida cortos (1-2 minutos) cuando rotas proxies para evitar conexiones obsoletas.
  • Polly es esencial para producción: Combina retry con backoff exponencial y circuit breaker para manejar fallos de proxies gracefully.
  • Parallel.ForEachAsync escala bien: Controla concurrencia con MaxDegreeOfParallelism y rota proxies thread-safe.
  • DI encapsula complejidad: Un servicio de pool de proxies hace tu código más limpio y testeable.
  • TLS requiere atención: Los proxies HTTPS pueden causar errores de certificado. Configura validación personalizada según tus necesidades de seguridad.

Con estos patrones, puedes construir scrapers robustos en .NET 8 que manejan proxies residenciales y datacenter de forma eficiente. ProxyHat proporciona el gateway gate.proxyhat.com:8080 con autenticación flexible por username para controlar país, ciudad y sesión sin cambiar tu configuración de código.

Para más detalles sobre casos de uso específicos, visita nuestra documentación de web scraping con proxies o revisa los países disponibles para geo-targeting.

¿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