Base de connaissances

Comment utiliser des proxys résidentiels en C# avec HttpClient

Les proxys en C# : WebProxy avec NetworkCredential, SocketsHttpHandler et PooledConnectionLifetime, IHttpClientFactory, et des clients par identité pour la rotation géo.

Chris Collins

Chris Collins

2 août 2026 · 8 min de lecture

.NET est un cheval de trait pour la collecte de données côté backend : I/O asynchrone rapide, typage fort, et un runtime qui gère la haute concurrence confortablement. Brancher un proxy résidentiel dans HttpClient, c’est quelques lignes, mais C# a son propre piège qui n’a rien à voir avec les proxys et tout à voir avec la façon dont HttpClient est censé être utilisé. Réussissez le cycle de vie du client et la partie proxy est facile.

Ceci est l’entrée C# de la même série que proxys résidentiels avec Python, avec Go et avec Node.js : le code qui marche, plus les écueils propres à .NET.

Tout ci-dessous utilise le gateway résidentiel de Shifter : un point de terminaison, p.shifter.io:443, avec tout le ciblage encodé dans le nom d’utilisateur. Changez l’hôte et les identifiants pour un autre fournisseur ; la forme est la même.

Le modèle du gateway en un paragraphe

Le nom d’utilisateur du proxy porte votre authentification et votre ciblage. Vous ne changez pas de point de terminaison pour changer de pays ou de session, vous changez la chaîne du nom d’utilisateur :

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

country-us cible les États-Unis, sid fixe une session sticky, ttl maintient cette IP pendant N secondes. Omettez sid/ttl et chaque nouvelle connexion tourne. Le mot de passe est constant. En .NET, ce nom d’utilisateur va dans un NetworkCredential, pas dans l’URL du proxy.

Le montage de base : WebProxy et un handler

Vous configurez le proxy sur un message handler et le passez à HttpClient. Les identifiants vont sur un WebProxy via NetworkCredential, pas en ligne dans l’URI.

using System.Net;
var user = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us";
var pass = Environment.GetEnvironmentVariable("SHIFTER_PASS");
var proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(user, pass) // pas de user:pass@ dans l'URL
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2) // voir le piege ci-dessous
};
var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var ip = await client.GetStringAsync("https://api.ipify.org");
Console.WriteLine(ip); // une IP résidentielle américaine

Deux choses à intérioriser. La chaîne user inclut les flags de ciblage (-country-us), parce que c’est là que vit la géo. Et les identifiants vont sur le WebProxy comme un NetworkCredential, HttpClient ne lira pas user:pass@host d’un URI de proxy comme un navigateur pourrait le faire.

Préférez SocketsHttpHandler (le handler par défaut sur .NET moderne) au plus ancien HttpClientHandler. C’est l’implémentation moderne et multiplateforme et elle expose PooledConnectionLifetime, dont vous avez besoin pour la raison ci-dessous.

Piège 1 : le problème du cycle de vie de HttpClient

C’est le piège spécifique à .NET, et il mord dans deux directions opposées.

Créez un new HttpClient() pour chaque requête et vous fuyez des sockets : chaque client détient son propre pool de connexions, et les clients libérés laissent des sockets coincés en TIME_WAIT. Sous charge, cela épuise les ports disponibles et votre app se met à lancer des SocketException. Le conseil bien connu est donc de réutiliser un seul HttpClient.

Mais un seul client gardé pour toujours a le problème inverse : il met en cache les résolutions DNS pour la durée du pool de connexions et ne remarque jamais quand l’IP d’un endpoint change. Le correctif n’est pas de choisir entre les deux, c’est PooledConnectionLifetime sur SocketsHttpHandler, qui recycle les connexions du pool selon un horaire pour que vous gardiez la réutilisation de connexion sans épingler un DNS périmé. Réglez-le à quelques minutes et réutilisez le client.

Réutiliser le client garde aussi les connexions chaudes à travers le proxy, ce qui est exactement le surcoût que le guide de latence existe pour éliminer.

La façon ASP.NET Core : IHttpClientFactory

Si vous êtes dans une app basée sur la DI, ne gérez pas HttpClient à la main du tout. IHttpClientFactory gère le cycle de vie du handler et le recyclage des connexions pour vous, et vous laisse configurer le proxy une fois sur un client nommé.

services.AddHttpClient("shifter")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(
Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-us",
Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true
});
// puis injectez IHttpClientFactory et appelez factory.CreateClient("shifter")

La factory fait tourner les handlers sous-jacents selon son propre horaire, donc vous obtenez le pooling et un DNS frais sans toucher PooledConnectionLifetime vous-même. C’est le chemin recommandé pour tout service de longue durée.

Piège 2 : le proxy est sur le handler, donc tournez avec un client par identité

Parce que le proxy et ses identifiants sont cuits dans le handler, vous ne pouvez pas varier le nom d’utilisateur du proxy par requête sur un seul HttpClient. Une identité différente signifie un handler différent, et donc un client différent. Le motif efficace est de mettre en cache un client par identité pour que chaque session garde son propre pool de connexions, la même idée qu’une map agent-par-identité dans d’autres stacks.

using System.Collections.Concurrent;
static readonly ConcurrentDictionary<string, HttpClient> Clients = new();
static HttpClient ClientFor(string country, string sid)
{
var key = $"{country}:{sid ?? "rotate"}";
return Clients.GetOrAdd(key, _ =>
{
var u = Environment.GetEnvironmentVariable("SHIFTER_USER") + "-country-" + country
+ (sid != null ? $"-sid-{sid}-ttl-600" : "");
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://p.shifter.io:443")
{
Credentials = new NetworkCredential(u, Environment.GetEnvironmentVariable("SHIFTER_PASS"))
},
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
return new HttpClient(handler);
});
}

Donnez à chaque unité logique de travail son propre sid et tournez entre unités, pas en milieu de flux (sticky vs rotatif couvre la distinction), et mappez le travail aux identités comme le décrit le billet sur la répartition de charge. Mettez en cache et réutilisez ces clients, n’en construisez pas un par requête.

Piège 3 : bornez votre concurrence

.NET rend trivial de tirer des milliers de requêtes d’un coup avec Task.WhenAll, et rien ne vous empêche d’ouvrir chaque connexion simultanément, ce qui épuise les sockets de votre côté et ressemble à une attaque pour la cible. Bornez les requêtes en vol avec un SemaphoreSlim :

var gate = new SemaphoreSlim(8); // au plus 8 requetes concurrentes
async Task<string> Fetch(HttpClient client, string url)
{
await gate.WaitAsync();
try { return await client.GetStringAsync(url); }
finally { gate.Release(); }
}

Bornez la concurrence par hôte cible, pas seulement globalement, pour qu’un site fragile ne soit pas martelé pendant qu’un permissif est affamé. Plus de parallélisme au-delà de la tolérance d’une cible achète des blocages, pas du débit (comment éviter de se faire bloquer).

Vérifiez que vous êtes bien sur le proxy

Avant de benchmarker ou de déboguer quoi que ce soit d’autre, confirmez l’IP de sortie :

var json = await client.GetStringAsync("http://ip-api.com/json");
Console.WriteLine(json); // attendez une IP résidentielle dans le pays ciblé

Votre propre IP signifie que le handler n’est pas appliqué (d’ordinaire UseProxy laissé à false, ou les identifiants sur le mauvais objet). Un blocage signifie que la sortie locale est bloquée. Les deux sont couverts dans le guide de diagnostic des timeouts.

FAQ

Pourquoi mes identifiants de proxy ne marchent-ils pas quand je les mets dans l’URL ? HttpClient ne lit pas user:pass@host d’un URI de proxy. Mettez les identifiants sur l’objet WebProxy comme un NetworkCredential. Parce que le gateway encode le ciblage dans le nom d’utilisateur, ce nom d’utilisateur complet (avec les flags -country-...) est le nom d’utilisateur du NetworkCredential, et le mot de passe est constant.

HttpClientHandler ou SocketsHttpHandler ? Préférez SocketsHttpHandler sur .NET moderne. C’est le handler géré par défaut, il est multiplateforme, et il expose PooledConnectionLifetime, dont vous avez besoin pour éviter un DNS périmé sur un client de longue vie. HttpClientHandler marche encore et lui délègue par en dessous.

Mon scraper .NET lance des SocketException sous charge. Pourquoi ? Vous créez presque certainement un new HttpClient() par requête et épuisez les sockets. Réutilisez un seul client (ou utilisez IHttpClientFactory), et réglez PooledConnectionLifetime pour que la réutilisation n’épingle pas un DNS périmé.

Comment faire tourner les IP par requête en C# ? Variez le nom d’utilisateur du proxy, ce qui signifie un handler différent et donc un client différent. Mettez en cache un HttpClient par identité dans un ConcurrentDictionary pour que chaque session garde son propre pool, et choisissez le client par unité de travail. Omettez le sid dans le nom d’utilisateur pour tourner à chaque nouvelle connexion.

IHttpClientFactory marche-t-il avec les proxys ? Oui. Configurez le proxy sur le handler primaire via ConfigurePrimaryHttpMessageHandler. La factory gère le cycle de vie du handler et la fraîcheur du DNS pour vous, donc c’est l’option la plus propre dans tout service basé sur la DI.

En résumé

C# plus proxys résidentiels est rapide dès que vous respectez deux choses : mettez les identifiants sur un WebProxy comme un NetworkCredential plutôt que dans l’URL, et réussissez le cycle de vie de HttpClient. Utilisez SocketsHttpHandler avec PooledConnectionLifetime, ou IHttpClientFactory dans une app avec DI, pour réutiliser les connexions sans épingler un DNS périmé et sans fuir de sockets. Faites tourner géo et sessions en mettant en cache un client par identité, et bornez la concurrence avec un SemaphoreSlim.

Réussissez cela et .NET gère la collecte concurrente aussi bien que n’importe quoi. Pointez-le vers le gateway résidentiel, et souvenez-vous que la qualité du pool décide à quelle fréquence vous réessayez tout court (réputation d’IP). La page tarifs propose les forfaits au Go pour le tester contre vos propres cibles.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de $0.75/GB.

Commencer