using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; public static class HttpHelper { private static readonly HttpClient DefaultClient; private static readonly HttpResult HttpResult; static HttpHelper() { var handler = CreateBestHandler(); TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); DefaultClient = new HttpClient(handler, disposeHandler: false) { Timeout = Timeout.InfiniteTimeSpan }; HttpResult = new HttpResult(); } private static HttpClient CreateProxyClient(string proxy) { var (address, isHttpsProxy) = ParseProxyScheme(proxy); var handler = CreateBestHandler(); TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); handler.Proxy = new WebProxy(address) { // 关键两行!解决 95% 的 HTTPS 代理报错 UseDefaultCredentials = false, BypassProxyOnLocal = false }; // 如果是 https:// 开头的代理(极少数 socks5 也可能用这个格式),强制走 CONNECT if (isHttpsProxy) { // .NET 6+ 推荐方式(安全又快) handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13; } handler.UseProxy = true; return new HttpClient(handler, disposeHandler: true) { Timeout = Timeout.InfiniteTimeSpan }; } private static HttpClientHandler CreateBestHandler() { return new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.All, AllowAutoRedirect = true, // 下面这行是终极救命稻草(解决代理返回 407 需要认证的坑) Proxy = null, // 先留空,后面再设 UseProxy = false, // 防止某些代理服务器返回奇怪的证书 ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true }; } // 智能解析代理地址,支持以下所有格式: // http://1.2.3.4:8888 // https://1.2.3.4:8888 // 1.2.3.4:8888 // user:pass@1.2.3.4:8888 private static (Uri address, bool isHttpsProxy) ParseProxyScheme(string proxy) { proxy = proxy.Trim(); var uri = proxy.Contains("://") ? new Uri(proxy) : new Uri("http://" + proxy); // 带用户名密码的代理 if (!string.IsNullOrEmpty(uri.UserInfo)) { var parts = uri.UserInfo.Split(new[] { ':' }, 2); var credential = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : ""); // 注意:这里不能直接设 DefaultProxyCredentials,会全局污染 // 我们改在下面用更安全的方式处理 } bool isHttps = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); var address = new Uri($"{(isHttps ? "http" : uri.Scheme)}://{uri.Host}:{uri.Port}"); return (address, isHttps); } public static async Task SendAsync( string url, HttpMethod method, Dictionary? headers = null, Dictionary? cookies = null, HttpContent? content = null, int timeoutSeconds = 30, string? proxy = null, CancellationToken cancellationToken = default) { var client = string.IsNullOrWhiteSpace(proxy) ? DefaultClient : CreateProxyClient(proxy); var shouldDispose = !string.IsNullOrWhiteSpace(proxy); // 处理带账号密码的代理(终极写法) if (!string.IsNullOrWhiteSpace(proxy) && proxy.Contains("@")) { var uri = new Uri(proxy.Contains("://") ? proxy : "http://" + proxy); if (!string.IsNullOrEmpty(uri.UserInfo)) { var parts = uri.UserInfo.Split(new[] { ':' }, 2); var credential = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : ""); // 关键:每个 HttpClientHandler 单独设置凭据,不污染全局 if (client.DefaultRequestHeaders.ProxyAuthorization == null) { var handler = (HttpClientHandler)client.GetType() .GetProperty("Handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) ?.GetValue(client)!; if (handler?.Proxy != null) { handler.Proxy.Credentials = credential; } } } } try { using var request = new HttpRequestMessage(method, url); if (content != null) request.Content = content; if (headers != null) { foreach (var h in headers) { if (IsContentHeader(h.Key)) request.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value); else request.Headers.TryAddWithoutValidation(h.Key, h.Value); } } if (cookies is { Count: > 0 }) { request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", cookies.Select(c => $"{c.Key}={c.Value}"))); } using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token); if (!response.IsSuccessStatusCode) { var err = await response.Content.ReadAsStringAsync(cts.Token); HttpResult.StatusCode = (int)response.StatusCode; HttpResult.Html = $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim(); return HttpResult; } var bytes = await response.Content.ReadAsByteArrayAsync(cts.Token); var charset = response.Content.Headers.ContentType?.CharSet; if (!string.IsNullOrEmpty(charset) && !charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) { try { HttpResult.StatusCode = (int)response.StatusCode; HttpResult.Html = Encoding.GetEncoding(charset).GetString(bytes); return HttpResult; } catch { } } HttpResult.StatusCode = (int)response.StatusCode; HttpResult.Html = Encoding.UTF8.GetString(bytes); return HttpResult; } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { HttpResult.StatusCode = 500; HttpResult.Html = $"请求超时({timeoutSeconds}s)"; return HttpResult; } finally { if (shouldDispose) client.Dispose(); } } // 你的三个快捷方法完全不变 public static Task GetAsync(string url, Dictionary? headers = null, Dictionary? cookies = null, int timeoutSeconds = 30, string? proxy = null) => SendAsync(url, HttpMethod.Get, headers, cookies, null, timeoutSeconds, proxy); public static Task PostFormAsync(string url, Dictionary form, Dictionary? headers = null, Dictionary? cookies = null, int timeoutSeconds = 30, string? proxy = null) { var content = new FormUrlEncodedContent(form); return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); } public static Task PostJsonAsync(string url, string json, Dictionary? headers = null, Dictionary? cookies = null, int timeoutSeconds = 30, string? proxy = null) { var content = new StringContent(json, Encoding.UTF8, "application/json"); headers ??= new(); headers["Content-Type"] = "application/json; charset=utf-8"; return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); } private static bool IsContentHeader(string name) => name.StartsWith("Content-", StringComparison.OrdinalIgnoreCase); private static void TrySetPooledConnectionLifetime(HttpClientHandler handler, TimeSpan lifetime) { try { var prop = handler.GetType().GetProperty("PooledConnectionLifetime"); prop?.SetValue(handler, lifetime); } catch { } } } public class HttpResult { public int StatusCode { get; set; } public string Html { get; set; } }