Guide Complet des Proxies HTTP en C# / .NET 8 : HttpClient, Rotation et Bonnes Pratiques

Apprenez à configurer HttpClient avec des proxies en C# .NET 8, implémenter la rotation d'IP, gérer les retries avec Polly, et sécuriser vos connexions TLS. Guide code-first avec exemples pratiques.

Guide Complet des Proxies HTTP en C# / .NET 8 : HttpClient, Rotation et Bonnes Pratiques

L'utilisation de proxies HTTP en C# est une compétence essentielle pour les développeurs qui construisent des applications de web scraping, d'automatisation ou de collecte de données. Avec .NET 8, Microsoft a introduit plusieurs optimisations qui rendent la gestion des proxies plus performante et plus flexible. Ce guide couvre tout, de la configuration basique d'HttpClient avec WebProxy jusqu'à l'implémentation d'un service de pool de proxies rotatifs avec injection de dépendances.

Pourquoi Utiliser des Proxies en C# ?

Les développeurs .NET ont plusieurs raisons d'intégrer des proxies dans leurs applications :

  • Éviter les limitations de taux — Les API et sites web limitent souvent le nombre de requêtes par IP.
  • Accès géo-restreint — Certaines ressources ne sont disponibles que dans des régions spécifiques.
  • Anonymat et sécurité — Masquer l'IP d'origine pour des tests ou de la recherche.
  • Scraping à grande échelle — Les proxies résidentiels C# permettent de distribuer les requêtes sur des milliers d'adresses IP.

Le défi technique principal est de configurer correctement HttpClient pour utiliser un proxy, gérer les rotations d'IP, et maintenir la fiabilité en production.

Configuration Basique : HttpClient avec HttpClientHandler et WebProxy

La méthode la plus directe pour utiliser un proxy HTTP en C# consiste à configurer HttpClientHandler avec un WebProxy. Voici un exemple complet avec authentification :

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

public class BasicProxyExample
{
    public static async Task Main()
    {
        // Configuration du proxy avec authentification
        var proxy = new WebProxy("http://gate.proxyhat.com:8080")
        {
            Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
            BypassProxyOnLocal = true
        };

        // Configuration du handler avec le proxy
        var handler = new HttpClientHandler
        {
            Proxy = proxy,
            UseProxy = true,
            // Important : autoriser les certificats auto-signés si nécessaire
            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
        };

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

        try
        {
            var response = await client.GetAsync("https://httpbin.org/ip");
            response.EnsureSuccessStatusCode();
            
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Réponse reçue : {content}");
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Erreur HTTP : {ex.Message}");
        }
    }
}

Liste de Bypass et Configuration Avancée

Le WebProxy permet de définir une liste d'URLs qui contourneront le proxy. C'est utile pour les ressources internes ou locales :

var bypassList = new[]
{
    "localhost",
    "127.0.0.1",
    "*.internal.company.com",
    "10.*",
    "192.168.*"
};

var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
    Credentials = new NetworkCredential("user-country-FR", "PASSWORD"),
    BypassList = bypassList,
    BypassProxyOnLocal = true
};

Attention : Ne créez pas une nouvelle instance d'HttpClient pour chaque requête. Utilisez IHttpClientFactory ou une instance statique réutilisable pour éviter l'épuisement des sockets.

SocketsHttpHandler : Contrôle Fin et PooledConnectionLifetime

Avec .NET 8, SocketsHttpHandler offre un contrôle plus granulaire sur les connexions. C'est particulièrement important pour les proxies rotatifs où vous devez gérer le cycle de vie des connexions poolées :

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

public class SocketsHandlerProxyExample
{
    private static readonly HttpClient _client;

    static SocketsHandlerProxyExample()
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential("user-session-sticky123", "PASSWORD")
            },
            UseProxy = true,
            // Rotation des connexions du pool toutes les 2 minutes
            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
            // Limite de connexions simultanées par endpoint
            MaxConnectionsPerServer = 100,
            // Activation de HTTP/2 si supporté
            EnableMultipleHttp2Connections = true,
            // Timeout de connexion
            ConnectTimeout = TimeSpan.FromSeconds(15)
        };

        _client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromMinutes(5)
        };
    }

    public static async Task<string?> FetchWithProxyAsync(string url, string session)
    {
        // Pour les proxies rotatifs, créer un handler dynamiquement
        var dynamicHandler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential($"user-session-{session}", "PASSWORD")
            },
            UseProxy = true,
            PooledConnectionLifetime = TimeSpan.FromMinutes(1)
        };

        using var dynamicClient = new HttpClient(dynamicHandler);
        
        try
        {
            var response = await dynamicClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Erreur pour session {session}: {ex.Message}");
            return null;
        }
    }

    public static async Task Main()
    {
        var tasks = Enumerable.Range(1, 5)
            .Select(i => FetchWithProxyAsync("https://httpbin.org/ip", $"session{i}"));
        
        var results = await Task.WhenAll(tasks);
        foreach (var result in results.Where(r => r != null))
        {
            Console.WriteLine(result);
        }
    }
}

Comparaison : HttpClientHandler vs SocketsHttpHandler

Caractéristique HttpClientHandler SocketsHttpHandler
Performance Bonne Excellente (implémentation native)
PooledConnectionLifetime Non supporté Supporté nativement
HTTP/2 Limité Support complet
Contrôle de connexion Basique Avancé (connect callbacks)
Compatibilité Toutes versions .NET .NET Core 2.1+

Gestion de la Résilience avec Polly

Les proxies peuvent échouer pour de nombreuses raisons : timeout, IP bloquée, serveur proxy indisponible. Polly est la bibliothèque standard .NET pour implémenter des stratégies de résilience. Installez les packages NuGet suivants :

dotnet add package Polly
_dotnet add package Polly.Extensions.Http

Voici une implémentation complète avec retry et circuit breaker :

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
using Polly.CircuitBreaker;

public class ResilientProxyClient
{
    private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
    private readonly IAsyncPolicy<HttpResponseMessage> _circuitBreakerPolicy;
    private readonly HttpClient _httpClient;

    public ResilientProxyClient(string proxyUsername, string proxyPassword)
    {
        // Politique de retry avec backoff exponentiel
        _retryPolicy = HttpPolicyExtensions
            .HandleTransientHttpError()
            .OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
            .OrResult(msg => msg.StatusCode == HttpStatusCode.Forbidden)
            .WaitAndRetryAsync(
                retryCount: 5,
                sleepDurationProvider: retryAttempt => 
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Retry {retryCount} après {timeSpan.TotalSeconds}s. " +
                        $"Raison: {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");
                });

        // Circuit breaker : ouvre après 5 échecs consécutifs
        _circuitBreakerPolicy = HttpPolicyExtensions
            .HandleTransientHttpError()
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1),
                onBreak: (outcome, breakDuration) =>
                {
                    Console.WriteLine($"Circuit ouvert pour {breakDuration.TotalMinutes} minutes");
                },
                onReset: () =>
                {
                    Console.WriteLine("Circuit réinitialisé");
                });

        // Configuration du handler avec proxy
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(proxyUsername, proxyPassword)
            },
            UseProxy = true,
            PooledConnectionLifetime = TimeSpan.FromMinutes(5)
        };

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

    public async Task<string?> ExecuteWithResilienceAsync(string url)
    {
        // Combinaison des politiques : retry puis circuit breaker
        var strategy = Policy.WrapAsync(_retryPolicy, _circuitBreakerPolicy);

        try
        {
            var response = await strategy.ExecuteAsync(async () =>
            {
                return await _httpClient.GetAsync(url);
            });

            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (BrokenCircuitException)
        {
            Console.WriteLine("Circuit ouvert - requêtes bloquées temporairement");
            return null;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Erreur finale: {ex.Message}");
            return null;
        }
    }

    // Rotation automatique du proxy en cas d'échec
    public async Task<string?> ExecuteWithProxyRotationAsync(string url, string[] sessions)
    {
        foreach (var session in sessions)
        {
            var client = CreateClientForSession(session);
            
            try
            {
                var response = await _retryPolicy.ExecuteAsync(async () =>
                {
                    return await client.GetAsync(url);
                });

                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsStringAsync();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Session {session} échouée: {ex.Message}");
            }
            finally
            {
                client.Dispose();
            }
        }

        return null;
    }

    private HttpClient CreateClientForSession(string session)
    {
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential($"user-session-{session}", "PASSWORD")
            },
            UseProxy = true,
            PooledConnectionLifetime = TimeSpan.FromMinutes(1)
        };

        return new HttpClient(handler);
    }
}

Scraping Concurrent avec Parallel.ForEachAsync

.NET 8 introduit Parallel.ForEachAsync, qui combine le parallélisme avec les avantages de l'async/await. C'est idéal pour le scraping à grande échelle avec des proxies :

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

public class ConcurrentProxyScraper
{
    private readonly string _proxyHost = "gate.proxyhat.com";
    private const int _proxyPort = 8080;
    private readonly string _password = "YOUR_PASSWORD";

    public async Task<Dictionary<string, string?>> ScrapeConcurrentlyAsync(
        string[] urls, 
        string[] countries,
        int maxDegreeOfParallelism = 10)
    {
        var results = new ConcurrentDictionary<string, string?>();
        var rateLimiter = new SlidingWindowRateLimiter(
            new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 50,
                Window = TimeSpan.FromSeconds(1),
                SegmentsPerWindow = 5
            });

        // Pool de clients HTTP par pays
        var clientsByCountry = new Dictionary<string, HttpClient>();
        foreach (var country in countries)
        {
            var handler = new SocketsHttpHandler
            {
                Proxy = new WebProxy($"http://{_proxyHost}:{_proxyPort}")
                {
                    Credentials = new NetworkCredential($"user-country-{country}", _password)
                },
                UseProxy = true,
                PooledConnectionLifetime = TimeSpan.FromMinutes(5),
                MaxConnectionsPerServer = 20
            };
            clientsByCountry[country] = new HttpClient(handler);
        }

        var random = new Random();

        await Parallel.ForEachAsync(urls, new ParallelOptions
        {
            MaxDegreeOfParallelism = maxDegreeOfParallelism
        }, async (url, cancellationToken) =>
        {
            // Rate limiting avant chaque requête
            using var lease = await rateLimiter.AcquireAsync(1, cancellationToken);
            
            if (!lease.IsAcquired)
            {
                Console.WriteLine($"Rate limit atteint pour {url}");
                results.TryAdd(url, null);
                return;
            }

            // Sélection aléatoire d'un pays/proxy
            var country = countries[random.Next(countries.Length)];
            var client = clientsByCountry[country];

            try
            {
                var response = await client.GetAsync(url, cancellationToken);
                
                if (response.IsSuccessStatusCode)
                {
                    var content = await response.Content.ReadAsStringAsync(cancellationToken);
                    results.TryAdd(url, content);
                    Console.WriteLine($"✓ {url} via proxy {country}");
                }
                else
                {
                    Console.WriteLine($"✗ {url}: Status {response.StatusCode}");
                    results.TryAdd(url, null);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"✗ {url}: {ex.Message}");
                results.TryAdd(url, null);
            }
        });

        // Cleanup
        foreach (var client in clientsByCountry.Values)
        {
            client.Dispose();
        }
        rateLimiter.Dispose();

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

    // Version avec gestion des timeouts et retries intégrés
    public async Task ScrapeWithTimeoutsAsync(string[] urls)
    {
        var options = new ParallelOptions
        {
            MaxDegreeOfParallelism = Environment.ProcessorCount * 2,
            CancellationToken = CancellationToken.None
        };

        await Parallel.ForEachAsync(urls, options, async (url, ct) =>
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
            
            var handler = new SocketsHttpHandler
            {
                Proxy = new WebProxy("http://gate.proxyhat.com:8080")
                {
                    Credentials = new NetworkCredential(
                        $"user-country-US-session-{Guid.NewGuid():N}",
                        "PASSWORD"
                    )
                },
                UseProxy = true,
                ConnectTimeout = TimeSpan.FromSeconds(10)
            };

            using var client = new HttpClient(handler);

            try
            {
                var response = await client.GetAsync(url, cts.Token);
                response.EnsureSuccessStatusCode();
                var content = await response.Content.ReadAsStringAsync(cts.Token);
                Console.WriteLine($"Succès: {url[..50]}...");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine($"Timeout: {url}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Erreur: {url} - {ex.Message}");
            }
        });
    }
}

Service de Pool de Proxies Rotatifs avec DI

Pour une architecture professionnelle, implémentez un service de pool de proxies avec injection de dépendances. Ce pattern permet de gérer efficacement la rotation, le suivi des IPs bloquées, et l'intégration avec les logs :

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

// Interface du service
public interface IProxyPoolService
{
    Task<HttpClient> GetClientAsync(string? countryCode = null);
    void MarkProxyAsBlocked(string sessionId);
    ProxyMetrics GetMetrics();
}

public record ProxyMetrics(
    int TotalRequests,
    int SuccessCount,
    int FailureCount,
    int BlockedProxies
);

// Configuration
public class ProxyPoolOptions
{
    public string Host { get; set; } = "gate.proxyhat.com";
    public int HttpPort { get; set; } = 8080;
    public int SocksPort { get; set; } = 1080;
    public string Username { get; set; } = "";
    public string Password { get; set; } = "";
    public string[] DefaultCountries { get; set; } = ["US", "GB", "DE", "FR"];
    public int MaxConnectionsPerProxy { get; set; } = 10;
    public TimeSpan ConnectionLifetime { get; set; } = TimeSpan.FromMinutes(5);
    public bool UseStickySessions { get; set; } = true;
}

// Implémentation
public class ProxyPoolService : IProxyPoolService, IDisposable
{
    private readonly ProxyPoolOptions _options;
    private readonly ILogger<ProxyPoolService> _logger;
    private readonly ConcurrentDictionary<string, HttpClient> _clients;
    private readonly ConcurrentBag<string> _blockedSessions;
    private readonly ConcurrentQueue<string> _sessionPool;
    private int _totalRequests;
    private int _successCount;
    private int _failureCount;

    public ProxyPoolService(ProxyPoolOptions options, ILogger<ProxyPoolService> logger)
    {
        _options = options;
        _logger = logger;
        _clients = new ConcurrentDictionary<string, HttpClient>();
        _blockedSessions = new ConcurrentBag<string>();
        _sessionPool = new ConcurrentQueue<string>();

        // Pré-générer un pool de sessions
        InitializeSessionPool(50);
    }

    private void InitializeSessionPool(int count)
    {
        for (int i = 0; i < count; i++)
        {
            var sessionId = Guid.NewGuid().ToString("N")[..8];
            _sessionPool.Enqueue(sessionId);
        }
    }

    public async Task<HttpClient> GetClientAsync(string? countryCode = null)
    {
        var country = countryCode ?? _options.DefaultCountries[
            Random.Shared.Next(_options.DefaultCountries.Length)];

        string sessionId;
        if (!_sessionPool.TryDequeue(out sessionId!))
        {
            sessionId = Guid.NewGuid().ToString("N")[..8];
        }

        // Pour les sessions sticky, réutiliser le client existant
        if (_options.UseStickySessions && _clients.TryGetValue(sessionId, out var existingClient))
        {
            return existingClient;
        }

        var username = _options.UseStickySessions
            ? $"{_options.Username}-country-{country}-session-{sessionId}"
            : $"{_options.Username}-country-{country}";

        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy($"http://{_options.Host}:{_options.HttpPort}")
            {
                Credentials = new NetworkCredential(username, _options.Password)
            },
            UseProxy = true,
            PooledConnectionLifetime = _options.ConnectionLifetime,
            MaxConnectionsPerServer = _options.MaxConnectionsPerProxy,
            ConnectTimeout = TimeSpan.FromSeconds(15),
            // Configuration TLS
            SslOptions = new SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    // En production, validez correctement les certificats
                    return true; // Pour les tests uniquement
                }
            }
        };

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

        // Ajouter des headers par défaut
        client.DefaultRequestHeaders.Add("User-Agent", 
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
        client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml");

        _clients.TryAdd(sessionId, client);
        _logger.LogInformation("Nouveau client proxy créé: Session={SessionId}, Country={Country}",
            sessionId, country);

        return client;
    }

    public void MarkProxyAsBlocked(string sessionId)
    {
        _blockedSessions.Add(sessionId);
        
        if (_clients.TryRemove(sessionId, out var client))
        {
            client.Dispose();
            _logger.LogWarning("Proxy bloqué et retiré: Session={SessionId}", sessionId);
        }

        Interlocked.Increment(ref _failureCount);
    }

    public ProxyMetrics GetMetrics()
    {
        return new ProxyMetrics(
            TotalRequests: _totalRequests,
            SuccessCount: _successCount,
            FailureCount: _failureCount,
            BlockedProxies: _blockedSessions.Count
        );
    }

    public void Dispose()
    {
        foreach (var client in _clients.Values)
        {
            client.Dispose();
        }
        _clients.Clear();
    }
}

// Extension pour l'enregistrement DI
public static class ProxyPoolServiceExtensions
{
    public static IServiceCollection AddProxyPool(
        this IServiceCollection services,
        Action<ProxyPoolOptions> configure)
    {
        services.Configure(configure);
        services.AddSingleton<IProxyPoolService, ProxyPoolService>();
        return services;
    }
}

// Utilisation dans Program.cs
// var builder = WebApplication.CreateBuilder(args);
// builder.Services.AddProxyPool(options =>
// {
//     options.Host = "gate.proxyhat.com";
//     options.HttpPort = 8080;
//     options.Username = "your_username";
//     options.Password = "your_password";
//     options.DefaultCountries = new[] { "US", "GB", "DE", "FR" };
// });

Configuration TLS et Certificate Pinning

La sécurité TLS est cruciale lors de l'utilisation de proxies, surtout pour les données sensibles. Voici comment configurer la validation des certificats et implémenter le certificate pinning :

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 : IDisposable
{
    private readonly HttpClient _client;
    private readonly HashSet<string> _allowedCertificates;

    public TlsProxyClient(string proxyUsername, string proxyPassword)
    {
        // Empreintes des certificats autorisés (SHA-256)
        _allowedCertificates = new HashSet<string>
        {
            "A1B2C3D4E5F6...", // Empreinte du certificat cible
            "123456789ABC..."   // Empreinte du certificat du proxy
        };

        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(proxyUsername, proxyPassword)
            },
            UseProxy = true,
            SslOptions = new SslClientAuthenticationOptions
            {
                // Certificate pinning personnalisé
                RemoteCertificateValidationCallback = ValidateCertificate,
                // Protocoles TLS supportés
                EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | 
                                      System.Security.Authentication.SslProtocols.Tls13,
                // Certificat client (si requis)
                ClientCertificates = new X509CertificateCollection(),
                // Vérification de la révocation
                CertificateRevocationCheckMode = X509RevocationMode.Online
            },
            PooledConnectionLifetime = TimeSpan.FromMinutes(5)
        };

        _client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(60)
        };
    }

    private bool ValidateCertificate(
        object sender,
        X509Certificate? certificate,
        X509Chain? chain,
        SslPolicyErrors policyErrors)
    {
        // Si aucun certificat, rejeter
        if (certificate == null) return false;

        // En développement, ignorer les erreurs de chaîne
#if DEBUG
        if (policyErrors == SslPolicyErrors.None)
            return true;
#endif

        // Extraire l'empreinte SHA-256
        var thumbprint = certificate.GetCertHashString();

        // Vérifier si le certificat est dans la liste autorisée
        if (_allowedCertificates.Contains(thumbprint))
        {
            Console.WriteLine($"Certificat validé: {thumbprint}");
            return true;
        }

        // En production, vérifier la chaîne complète
        if (chain != null)
        {
            var chainPolicy = new X509ChainPolicy
            {
                RevocationMode = X509RevocationMode.Online,
                RevocationFlag = X509RevocationFlag.ExcludeRoot,
                VerificationFlags = X509VerificationFlags.NoFlag
            };

            chain.ChainPolicy = chainPolicy;
            
            if (chain.Build(certificate as X509Certificate2))
            {
                // Vérifier que le certificat n'a pas expiré
                if (DateTime.UtcNow <= certificate.GetExpirationDateString())
                {
                    return true;
                }
            }
        }

        Console.WriteLine($"Certificat rejeté: {thumbprint}, Erreurs: {policyErrors}");
        return false;
    }

    // Configuration avec certificat racine personnalisé
    public static HttpClient CreateClientWithCustomRootCA(
        string proxyUsername,
        string proxyPassword,
        string rootCaPath)
    {
        var rootCa = new X509Certificate2(rootCaPath);
        
        var handler = new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(proxyUsername, proxyPassword)
            },
            UseProxy = true,
            SslOptions = new SslClientAuthenticationOptions
            {
                RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
                {
                    if (cert == null || chain == null) return false;

                    // Ajouter le certificat racine personnalisé à la chaîne
                    chain.ChainPolicy.ExtraStore.Add(rootCa);
                    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

                    return chain.Build(cert as X509Certificate2);
                }
            }
        };

        return new HttpClient(handler);
    }

    public async Task<string?> GetAsync(string url)
    {
        try
        {
            var response = await _client.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
        catch (HttpRequestException ex) when (ex.InnerException is System.Security.Authentication.AuthenticationException)
        {
            Console.WriteLine($"Erreur TLS: {ex.Message}");
            throw new SecurityException("Échec de la validation du certificat", ex);
        }
    }

    public void Dispose()
    {
        _client.Dispose();
    }
}

Intégration avec IHttpClientFactory

La meilleure pratique en .NET moderne est d'utiliser IHttpClientFactory avec des handlers nommés ou typés :

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using System.Net;

// Program.cs - Configuration
var builder = WebApplication.CreateBuilder(args);

// Configuration du client proxy via IHttpClientFactory
builder.Services.AddHttpClient("ProxyClient")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(
                    builder.Configuration["Proxy:Username"],
                    builder.Configuration["Proxy:Password"]
                )
            },
            UseProxy = true,
            PooledConnectionLifetime = TimeSpan.FromMinutes(5)
        };
    })
    .AddPolicyHandler(GetRetryPolicy());

// Client typé pour le scraping
builder.Services.AddHttpClient<IScraperService, WebScraperService>("ScraperClient")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler
        {
            Proxy = new WebProxy("http://gate.proxyhat.com:8080")
            {
                Credentials = new NetworkCredential(
                    builder.Configuration["Proxy:Username"],
                    builder.Configuration["Proxy:Password"]
                )
            },
            UseProxy = true,
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    });

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

// Service de scraping
public interface IScraperService
{
    Task<string?> ScrapeAsync(string url);
}

public class WebScraperService : IScraperService
{
    private readonly HttpClient _client;

    public WebScraperService(HttpClient client)
    {
        _client = client;
    }

    public async Task<string?> ScrapeAsync(string url)
    {
        var response = await _client.GetAsync(url);
        return response.IsSuccessStatusCode 
            ? await response.Content.ReadAsStringAsync() 
            : null;
    }
}

Points Clés à Retenir

Points clés pour les proxies HTTP en C# / .NET 8 :

  • Utilisez SocketsHttpHandler — Plus performant et offre PooledConnectionLifetime pour la rotation des connexions.
  • Ne créez pas de nouveaux HttpClient — Utilisez IHttpClientFactory ou des instances statiques avec handlers configurables.
  • Implémentez Polly — Retry avec backoff exponentiel et circuit breaker sont essentiels pour la fiabilité.
  • Rotation des sessions — Utilisez des identifiants de session uniques pour distribuer les requêtes sur différentes IPs.
  • Configuration TLS — Validez correctement les certificats, surtout en production avec des proxies résidentiels.
  • Rate limiting — Implémentez des limites de taux pour éviter de surcharger les cibles ou les proxies.

Conclusion

La configuration des proxies HTTP en C# avec .NET 8 offre une flexibilité et des performances remarquables. En combinant SocketsHttpHandler avec Polly pour la résilience, Parallel.ForEachAsync pour la concurrence, et un service de pool de proxies avec DI, vous obtenez une architecture robuste pour le scraping et l'automatisation à grande échelle.

Les proxies résidentiels C# sont particulièrement adaptés au scraping de données sensibles car ils utilisent de vraies adresses IP résidentielles, rendant la détection plus difficile. Pour les cas d'usage nécessitant un ciblage géographique précis, consultez notre page des locations de proxies ou explorez nos cas d'usage de web scraping.

Pour les projets nécessitant une rotation d'IP fréquente, envisagez d'utiliser les sessions sticky de ProxyHat qui maintiennent la même IP pendant une durée configurable, idéal pour les workflows multi-étapes comme la connexion à des sites ou les processus de checkout.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog