Używanie proxy HTTP w C# to podstawa web scrapingu, testowania API i automatyzacji — ale .NET ma kilka warstw abstrakcji, które łatwo skonfigurować błędnie. HttpClient, HttpClientHandler, SocketsHttpHandler, WebProxy, IWebProxy — która klasica za co odpowiada? Jak rotować IP bez wycieku połączeń? Jak obsłużyć błędy z Polly i skalować z Parallel.ForEachAsync?
Ten artykuł to przewodnik kod-first dla .NET 8+. Pokażę sześć kompletnych, uruchamialnych przykładów: od podstawowego HttpClient z proxy, przez zaawansowaną konfigurację TLS, aż po produkcyjną pulę rotujących proxy z dependency injection.
1. Podstawowy C# HTTP Proxy z HttpClient i HttpClientHandler
Klasa HttpClientHandler to najprostszy sposób na skonfigurowanie proxy w .NET. Dziedziczy z HttpMessageHandler i udostępnia właściwości takie jak Proxy, UseProxy, Credentials i Proxy.
Pod maską HttpClientHandler deleguje do SocketsHttpHandler w .NET Core 2.1+, ale API pozostaje kompatybilne z klasycznym .NET Framework.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
public class BasicProxyExample
{
private readonly HttpClient _httpClient;
public BasicProxyExample()
{
// Konfiguracja WebProxy z poświadczeniami
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
BypassProxyOnLocal = true
};
// HttpClientHandler z proxy
var handler = new HttpClientHandler
{
Proxy = proxy,
UseProxy = true,
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
// HttpClient z czasem życia połączenia
_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($"Błąd HTTP: {ex.Message}");
throw;
}
catch (TaskCanceledException)
{
Console.WriteLine("Timeout — proxy może być wolne lub niedostępne");
throw;
}
}
}
// Użycie
var example = new BasicProxyExample();
var html = await example.FetchAsync("https://httpbin.org/ip");
Console.WriteLine(html);
Kluczowe parametry ProxyHat:
- Host:
gate.proxyhat.com - Port HTTP:
8080 - Port SOCKS5:
1080 - Format URL:
http://USERNAME:PASSWORD@gate.proxyhat.com:8080
Dla C# residential proxies, username zawiera flagi geo-targetingu: user-country-US, user-country-DE-city-berlin, lub user-session-abc123 dla sticky sessions.
Bypass List — kiedy pomijać proxy
Właściwość BypassProxyOnLocal pomija proxy dla adresów lokalnych. Dla większej kontroli użyj BypassList:
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD"),
BypassProxyOnLocal = true,
BypassList = new[]
{
"localhost",
"127.0.0.1",
"*.internal.company.com",
"10.*",
"192.168.*"
}
};
2. SocketsHttpHandler i PooledConnectionLifetime — zaawansowane połączenia
SocketsHttpHandler to nowoczesny handler wprowadzony w .NET Core 2.1. Daje precyzyjną kontrolę nad pulą połączeń, TLS i timeoutami. W przeciwieństwie do HttpClientHandler, pozwala na per-request proxy przez właściwość Proxy w HttpRequestMessage.
Kluczowa właściwość to PooledConnectionLifetime — czas, po którym połączenie jest usuwane z puli, nawet jeśli jest aktywne. To krytyczne dla C# HTTP proxy z rotacją IP: stare połączenia mogą mieć przypisane IP, które już nie działa.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
public class SocketsHandlerExample
{
private readonly HttpClient _httpClient;
public SocketsHandlerExample()
{
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080"),
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2), // Rotuj połączenia co 2 min
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 100,
EnableMultipleHttp2Connections = true,
AutomaticDecompression = DecompressionMethods.All
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
}
// Per-request proxy — nadpisuje domyślne proxy handlera
public async Task<string> FetchWithCustomProxyAsync(
string url,
string proxyUrl,
string username,
string password)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Ustaw proxy dla tego konkretnego żądania
var proxy = new WebProxy(proxyUrl)
{
Credentials = new NetworkCredential(username, password)
};
// W .NET 8+ można ustawić Proxy na HttpRequestMessage
// ale wymaga to custom message handlera
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(1)
};
using var client = new HttpClient(handler);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string> FetchAsync(string url)
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
// Przykład z ProxyHat residential proxy
var example = new SocketsHandlerExample();
var result = await example.FetchWithCustomProxyAsync(
"https://httpbin.org/ip",
"http://gate.proxyhat.com:8080",
"user-country-DE-city-berlin",
"YOUR_PASSWORD"
);
Kiedy używać SocketsHttpHandler zamiast HttpClientHandler?
| Cecha | HttpClientHandler | SocketsHttpHandler |
|---|---|---|
| Kontrola puli połączeń | Ograniczona | Pełna (PooledConnectionLifetime) |
| Per-request proxy | Wymaga custom handlera | Możliwe przez delegaty |
| HTTP/2 | Wspierane | Lepsza kontrola multiple connections |
| Wydajność | Dobra | Najlepsza (bez warstwy HttpClientHandler) |
| Kompatybilność | .NET Framework + Core | .NET Core 2.1+ / .NET 5+ |
3. Polly — Retry i Circuit Breaker dla proxy HTTP
Proxy residential mogą być niestabilne — IP są blokowane, serwery mają przestoje, rate limity odrzucają żądania. Polly to biblioteka .NET do obsługi błędów transitoryjnych z retry, circuit breaker, timeout i bulkhead.
Zainstaluj przez NuGet:
dotnet add package Polly
dotnet add package Polly.Extensions.Http
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
public class ResilientProxyClient
{
private readonly HttpClient _httpClient;
private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
public ResilientProxyClient()
{
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
// Policy retry: 3 próby z exponential backoff + jitter
_retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() // 5xx, 408, network failures
.Or<TaskCanceledException>() // Timeout
.OrResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.TooManyRequests || // 429
r.StatusCode == HttpStatusCode.Forbidden) // 403 - może być blokada IP
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) +
TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000))); // Jitter
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<string> FetchWithRetryAsync(string url)
{
var response = await _retryPolicy.ExecuteAsync(async () =>
{
var result = await _httpClient.GetAsync(url);
result.EnsureSuccessStatusCode();
return result;
});
return await response.Content.ReadAsStringAsync();
}
// Circuit Breaker — otwiera obwód po 5 kolejnych błędach
public IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (outcome, duration) =>
{
Console.WriteLine($"Circuit otwarty na {duration.TotalSeconds}s: {outcome.Exception?.Message}");
},
onReset: () => Console.WriteLine("Circuit zamknięty — działamy"));
}
// Połącz retry + circuit breaker
public async Task<string> FetchResilientAsync(string url)
{
var retryPolicy = _retryPolicy;
var circuitBreaker = GetCircuitBreakerPolicy();
var strategy = Policy.WrapAsync(retryPolicy, circuitBreaker);
var response = await strategy.ExecuteAsync(async () =>
{
var result = await _httpClient.GetAsync(url);
return result;
});
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
var client = new ResilientProxyClient();
var data = await client.FetchResilientAsync("https://example.com/api/data");
4. Parallel.ForEachAsync — równoległy web scraping z proxy
Parallel.ForEachAsync wprowadzony w .NET 6 to nowoczesny sposób na równoległe przetwarzanie z automatycznym zarządzaniem przez ParallelOptions i TaskScheduler. Idealny do scrapingu z .NET HttpClient proxy.
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
public class ParallelScraper
{
private readonly HttpClient _httpClient;
public ParallelScraper()
{
var proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 50 // Limit równoległych połączeń
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public async Task<Dictionary<string, string>> ScrapeUrlsAsync(IEnumerable<string> urls, int maxConcurrency = 10)
{
var results = new ConcurrentDictionary<string, string>();
var errors = new ConcurrentBag<Exception>();
var options = new ParallelOptions
{
MaxDegreeOfParallelism = maxConcurrency
};
await Parallel.ForEachAsync(urls, options, async (url, cancellationToken) =>
{
try
{
var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
results.TryAdd(url, content);
Console.WriteLine($"[OK] {url} - {content.Length} znaków");
}
catch (Exception ex)
{
errors.Add(ex);
Console.WriteLine($"[BŁĄD] {url} - {ex.Message}");
}
});
if (errors.Count > 0)
{
Console.WriteLine($"Zakończono z {errors.Count} błędami");
}
return new Dictionary<string, string>(results);
}
}
// Użycie
var urls = new List<string>
{
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://example.com",
"https://httpbin.org/get"
};
var scraper = new ParallelScraper();
var results = await scraper.ScrapeUrlsAsync(urls, maxConcurrency: 5);
foreach (var (url, content) in results)
{
Console.WriteLine($"{url}: {content.Length} znaków");
}
Zarządzanie współbieżnością
- MaxDegreeOfParallelism: kontroluje liczbę równoległych zadań
- MaxConnectionsPerServer w SocketsHttpHandler: limit połączeń do jednego hosta
- Rate limiting: szanuj
Retry-Afteri nagłówki rate limit
Dla C# residential proxies, ProxyHat automatycznie rotuje IP, więc możesz zwiększyć współbieżność bez blokad.
5. Rotating Proxy Pool Service z Dependency Injection
W aplikacji produkcyjnej potrzebujesz serwisu, który zarządza pulą proxy, rotuje IP i integruje się z DI kontenerem .NET. Oto kompletna implementacja:
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
public interface IProxyPool
{
string GetNextProxyUsername();
HttpClient CreateClient(string? proxyUsername = null);
}
public class ProxyHatPool : IProxyPool, IDisposable
{
private readonly ConcurrentQueue<string> _proxyUsernames;
private readonly string _password;
private readonly string _baseUrl;
private readonly ILogger<ProxyHatPool> _logger;
private readonly ConcurrentDictionary<string, HttpClient> _clientCache;
private readonly TimeSpan _connectionLifetime = TimeSpan.FromMinutes(2);
public ProxyHatPool(
string usernamePrefix,
string password,
string[] countries,
ILogger<ProxyHatPool> logger)
{
_password = password;
_baseUrl = "http://gate.proxyhat.com:8080";
_logger = logger;
_clientCache = new ConcurrentDictionary<string, HttpClient>();
// Generuj listę username'ów dla różnych krajów
_proxyUsernames = new ConcurrentQueue<string>();
foreach (var country in countries)
{
for (int i = 0; i < 10; i++)
{
// Format: user-country-US-session-abc123
var sessionId = Guid.NewGuid().ToString("N")[..8];
var username = $"{usernamePrefix}-country-{country}-session-{sessionId}";
_proxyUsernames.Enqueue(username);
}
}
_logger.LogInformation("Zainicjalizowano pulę {Count} proxy", _proxyUsernames.Count);
}
public string GetNextProxyUsername()
{
if (_proxyUsernames.TryDequeue(out var username))
{
_proxyUsernames.Enqueue(username); // Rotuj z powrotem
return username;
}
throw new InvalidOperationException("Pula proxy jest pusta");
}
public HttpClient CreateClient(string? proxyUsername = null)
{
proxyUsername ??= GetNextProxyUsername();
// Sprawdź cache — każdy username ma własnego klienta
if (_clientCache.TryGetValue(proxyUsername, out var cachedClient))
{
return cachedClient;
}
var proxy = new WebProxy(_baseUrl)
{
Credentials = new NetworkCredential(proxyUsername, _password)
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = _connectionLifetime,
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 20,
AutomaticDecompression = DecompressionMethods.All
};
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
_clientCache.TryAdd(proxyUsername, client);
_logger.LogDebug("Utworzono HttpClient dla proxy {Username}", proxyUsername);
return client;
}
public void Dispose()
{
foreach (var client in _clientCache.Values)
{
client.Dispose();
}
}
}
// Rejestracja w DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddProxyPool(
this IServiceCollection services,
string usernamePrefix,
string password,
string[] countries)
{
services.AddSingleton<IProxyPool>(sp =>
{
var logger = sp.GetRequiredService<ILogger<ProxyHatPool>>();
return new ProxyHatPool(usernamePrefix, password, countries, logger);
});
return services;
}
}
// Użycie w Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
services.AddProxyPool(
usernamePrefix: "user",
password: "YOUR_PASSWORD",
countries: new[] { "US", "DE", "GB", "FR" }
);
var provider = services.BuildServiceProvider();
var proxyPool = provider.GetRequiredService<IProxyPool>();
// Użycie w serwisie aplikacji
for (int i = 0; i < 5; i++)
{
var client = proxyPool.CreateClient();
var ip = await client.GetStringAsync("https://httpbin.org/ip");
Console.WriteLine($"Request {i}: {ip}");
}
Zalety puli proxy z DI
- Rotacja automatyczna: każde żądanie może używać innego IP
- Cache klientów: HttpClient jest ponownie używany dla tego samego username
- Zarządzanie czasem życia: PooledConnectionLifetime zapobiega wyciekom połączeń
- Logowanie: integracja z ILogger dla monitoringu
6. TLS, SslClientAuthenticationOptions i Certificate Pinning
Przy pracy z proxy musisz rozważyć bezpieczeństwo TLS. Czy ufasz certyfikatowi proxy? Czy serwer docelowy akceptuje połączenia przez proxy? Oto jak skonfigurować TLS w .NET 8.
Custom Root CA — zaufanie własnemu certyfikatowi
Niektóre proxy (zwłaszcza datacenter) używają MITM do inspekcji ruchu. Musisz dodać ich CA do zaufanych:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
public class TlsProxyClient
{
private readonly HttpClient _httpClient;
private readonly X509Certificate2 _customRootCa;
public TlsProxyClient(string proxyUrl, string username, string password, string rootCaPath)
{
// Załaduj custom root CA
_customRootCa = new X509Certificate2(rootCaPath);
var proxy = new WebProxy(proxyUrl)
{
Credentials = new NetworkCredential(username, password)
};
var handler = new SocketsHttpHandler
{
Proxy = proxy,
UseProxy = true,
SslOptions = new SslClientAuthenticationOptions
{
EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12,
// Custom walidacja certyfikatu
RemoteCertificateValidationCallback = ValidateCertificate,
// Certificate pinning
TargetHost = "your-target-host.com"
},
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
};
_httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
private bool ValidateCertificate(
object sender,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
// Jeśli nie ma błędów, akceptuj
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
// Jeśli błąd to niezaufany root, sprawdź custom CA
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && chain != null)
{
// Dodaj custom CA do chain
chain.ChainPolicy.ExtraStore.Add(_customRootCa);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
if (chain.Build(certificate!))
{
Console.WriteLine("Certyfikat zweryfikowany przez custom CA");
return true;
}
}
Console.WriteLine($"Błąd walidacji certyfikatu: {sslPolicyErrors}");
return false;
}
// Certificate pinning — tylko konkretny certyfikat
public async Task<string> FetchWithPinningAsync(string url, string expectedCertThumbprint)
{
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://gate.proxyhat.com:8080")
{
Credentials = new NetworkCredential("user-country-US", "PASSWORD")
},
UseProxy = true,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
if (cert == null) return false;
var thumbprint = cert.GetCertHashString();
var isValid = thumbprint.Equals(expectedCertThumbprint, StringComparison.OrdinalIgnoreCase);
if (!isValid)
{
Console.WriteLine($"Certificate pinning failed! Expected: {expectedCertThumbprint}, Got: {thumbprint}");
}
return isValid;
}
}
};
using var client = new HttpClient(handler);
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
public async Task<string> FetchAsync(string url)
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
// Użycie
var client = new TlsProxyClient(
"http://gate.proxyhat.com:8080",
"user-country-US",
"PASSWORD",
"path/to/root-ca.crt"
);
var data = await client.FetchAsync("https://example.com");
Najlepsze praktyki TLS z proxy
- TLS 1.3: używaj najnowszego protokołu gdy to możliwe
- Certificate pinning: dla krytycznych endpointów, weryfikuj thumbprint certyfikatu
- Custom CA: dodaj do zaufanych tylko jeśli kontrolujesz proxy
- Wyjątki walidacji: loguj wszystkie błędy SSL dla debugowania
Uwaga: Residential proxy jak ProxyHat zazwyczaj nie wykonują MITM — ruch jest szyfrowany end-to-end. Certificate pinning jest potrzebny głównie przy datacenter proxy z inspekcją SSL.
Porównanie podejść do proxy w .NET 8
| Podejście | Złożoność | Elastyczność | Wydajność | Use case |
|---|---|---|---|---|
| HttpClientHandler | Niska | Średnia | Dobra | Proste skrypty, jednorazowe proxy |
| SocketsHttpHandler | Średnia | Wysoka | Najlepsza | Produkcja, pule połączeń |
| Polly + Retry | Średnia | Wysoka | Dobra | Niestabilne proxy, rate limiting |
| Parallel.ForEachAsync | Średnia | Średnia | Wysoka | Masowy scraping |
| DI Proxy Pool | Wysoka | Najwyższa | Najlepsza | Aplikacje enterprise, rotacja IP |
Key Takeaways
- HttpClientHandler to najprostszy sposób na C# HTTP proxy, ale SocketsHttpHandler daje większą kontrolę nad pulą połączeń.
- PooledConnectionLifetime jest krytyczny dla rotujących proxy — usuwa stare połączenia z przypisanymi IP.
- Polly z retry i circuit breaker chroni przed niestabilnymi C# residential proxies.
- Parallel.ForEachAsync pozwala na skalowanie scrapingu z kontrolą współbieżności.
- DI Proxy Pool to wzorzec produkcyjny — zarządza rotacją, cache'uje klienty i integruje się z ILogger.
- TLS certificate pinning jest ważny przy datacenter proxy z MITM; residential proxy jak ProxyHat nie wymagają custom CA.
Często zadawane pytania
Czy mogę używać SOCKS5 z HttpClient w .NET 8?
Tak, .NET 8 obsługuje SOCKS5 natywnie przez SocketsHttpHandler. Użyj URL socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080 jako proxy. Wymaga to jednak custom konfiguracji — standardowy WebProxy obsługuje tylko HTTP/HTTPS.
Jak uniknąć wyciek DNS przy używaniu proxy?
DNS leaks występują, gdy aplikacja rozwiązuje hostname lokalnie zamiast przez proxy. W .NET, HttpClient z proxy rozwiązuje hostname przez serwer proxy (dla HTTP proxy). Dla SOCKS5, użyj SocketsHttpHandler z ConnectCallback dla pełnej kontroli nad resolucją DNS.
Czy HttpClient powinien być singletonem?
Tak, HttpClient powinien być używany jako singleton lub przez IHttpClientFactory. Tworzenie nowego HttpClient dla każdego żądania wyczerpuje porty TCP. Przy proxy, używaj PooledConnectionLifetime aby regularnie odświeżać połączenia.
Jak rotować IP bez tworzenia nowych HttpClient?
Użyj sticky sessions z ProxyHat: username user-session-abc123 przydziela stałe IP dla sesji. Dla rotacji per-request, używaj puli proxy z DI — każde żądanie może używać innego HttpClient z innym username.
Jak debugować problemy z proxy w .NET?
Włącz System.Net.Http.Logging w appsettings.json dla szczegółowych logów. Użyj curl z tym samym proxy URL do weryfikacji. Sprawdź czy PooledConnectionLifetime nie jest za krótki (reconnect overhead) lub za długi (stale IP).
Następne kroki
Przetestuj te przykłady z ProxyHat residential proxies — oferują geo-targeting, sticky sessions i wysoką dostępność. Zobacz ceny ProxyHat i dostępne lokalizacje.
Dla większych projektów scrapingowych, przeczytaj nasz przewodnik najlepszych praktyk web scrapingu i SERP tracking.






