ScreenshotNeo

BlogHow-to

How to Use a Proxy with HttpClient in C#

Configure per-client and global proxies in C# with runnable code, authentication, bypass rules, reuse guidance, troubleshooting, and ScreenshotNeo.

By the ScreenshotNeo team29 September 20269 min read

How to Use a Proxy with HttpClient in C#

To route one C# HttpClient through an HTTP proxy, create a WebProxy, assign it to HttpClientHandler.Proxy, and construct the client with that handler. The proxy setting belongs to the handler, so reuse the handler and client for the intended lifetime instead of creating a new client for every request.

This article covers per-client proxies, global defaults, environment variables, authentication, bypass rules, disabling proxies, .NET client lifetime, diagnostics, and production considerations. The APIs are built into .NET; replace the example endpoint with the proxy supplied by your network or hosting environment.

Configure a proxy per HttpClient

A per-client proxy is the clearest option when only one outbound client, destination group, or tenant should use a particular proxy. HttpClientHandler.Proxy accepts an IWebProxy; WebProxy is the built-in implementation documented by Microsoft.

A per-client HttpClient handler routes requests through the configured proxy before reaching the destination.
A per-client HttpClient handler routes requests through the configured proxy before reaching the destination.
using System.Net;
using System.Net.Http;

var proxy = new WebProxy("http://proxy.example:8080");
var handler = new HttpClientHandler
{
    Proxy = proxy
};

using var client = new HttpClient(handler);
using var response = await client.GetAsync("https://example.com");
response.EnsureSuccessStatusCode();

var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);

The proxy URL in this example is illustrative. Use the hostname and port supplied by your environment. Microsoft’s API reference describes the property and its default behavior in HttpClientHandler.Proxy, while the WebProxy class reference documents constructors and proxy settings.

Use a proxy with HttpClientFactory

In ASP.NET Core, register a named or typed client. The handler is created by the factory and pooled, while your application receives a configured client.

using System.Net;

builder.Services.AddHttpClient("through-proxy")
    .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
    {
        Proxy = new WebProxy("http://proxy.example:8080")
    });

// Later, in a service or controller:
public sealed class FetchService(IHttpClientFactory factory)
{
    public async Task<string> GetAsync(CancellationToken cancellationToken)
    {
        var client = factory.CreateClient("through-proxy");
        return await client.GetStringAsync("https://example.com", cancellationToken);
    }
}

Use a separate named client when you need a separate proxy. A handler has one proxy configuration; applications that rotate between several proxies generally need several deliberately configured clients.

Set proxy credentials safely

If the proxy requires authentication, assign credentials on the proxy object. Keep usernames, passwords, and tokens in environment variables, a secret store, or your deployment configuration. Do not commit them to source code or print them in logs.

using System.Net;
using System.Net.Http;

var username = Environment.GetEnvironmentVariable("PROXY_USER");
var password = Environment.GetEnvironmentVariable("PROXY_PASSWORD");

if (string.IsNullOrWhiteSpace(username) || password is null)
    throw new InvalidOperationException("Proxy credentials are not configured.");

var proxy = new WebProxy("http://proxy.example:8080")
{
    Credentials = new NetworkCredential(username, password)
};

using var handler = new HttpClientHandler { Proxy = proxy };
using var client = new HttpClient(handler);
using var response = await client.GetAsync("https://example.com");
response.EnsureSuccessStatusCode();

IWebProxy exposes credentials and bypass behavior; see Microsoft’s IWebProxy documentation. The exact authentication method accepted by a proxy is an infrastructure concern, so confirm whether it expects basic credentials, integrated credentials, or another mechanism.

Default credentials

Some managed environments use the current process or Windows identity. WebProxy supports default credentials, but only enable them when your network explicitly requires integrated authentication and the process identity is appropriate.

var proxy = new WebProxy("http://proxy.example:8080")
{
    UseDefaultCredentials = true
};

Configure a global default proxy

Use HttpClient.DefaultProxy when clients without an explicit handler proxy should share one default. A handler-level proxy still takes precedence for that client.

using System.Net;
using System.Net.Http;

HttpClient.DefaultProxy = new WebProxy("http://proxy.example:8080");

using var client = new HttpClient();
using var response = await client.GetAsync("https://example.com");
response.EnsureSuccessStatusCode();

Default initialization differs by platform. Microsoft documents that Windows checks environment variables first and otherwise user proxy settings; macOS checks environment variables first and otherwise system settings; Linux checks environment variables first and otherwise starts with a nonconfigured proxy that bypasses all addresses. Read the platform-specific behavior in Microsoft’s HttpClient proxy guidance before assuming a machine setting will be used.

Use HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY

Environment variables are useful in containers and deployments where code should remain unchanged:

HTTP_PROXY=http://proxy.example:8080
HTTPS_PROXY=http://proxy.example:8080
ALL_PROXY=http://proxy.example:8080
NO_PROXY=localhost,127.0.0.1,.internal.example.com
  • HTTP_PROXY applies to HTTP requests.
  • HTTPS_PROXY applies to HTTPS requests.
  • ALL_PROXY is the fallback when a scheme-specific variable is absent.
  • NO_PROXY is a comma-separated bypass list.

Microsoft’s documented matching rules have details that often cause surprises. A leading period matches subdomains: .example.com matches www.example.com but not the bare example.com. The entry example.com does not match www.example.com. Asterisks are not supported as wildcards. On case-sensitive systems, both upper- and lowercase names may be present, with lowercase checked first.

For the documented .NET proxy environment format, the value may be a hostname or IP address with an optional port, or an http URL that can include credentials. The proxy setting must start with http, have no path after the host and port, and this requirement is separate from whether the destination URL is HTTP or HTTPS.

Control bypass behavior

A proxy can be bypassed for local destinations or hosts covered by bypass rules. If a request appears to go directly to the destination, inspect NO_PROXY, local-host rules, and any bypass list configured on WebProxy.

using System.Net;

var proxy = new WebProxy("http://proxy.example:8080")
{
    BypassList = new[]
    {
        "localhost",
        "127.0.0.1",
        ".internal.example.com"
    },
    BypassProxyOnLocal = true
};

Flat hostnames, loopback or local IP addresses, and hosts matching the local computer’s domain suffix can be treated as local under the documented handler behavior. Verify the effective configuration in the running environment rather than relying on assumptions from a development machine.

Explicitly disable proxying

Setting Proxy to null is not the documented instruction for an explicit no-proxy client. Assign the empty proxy returned by GlobalProxySelection.GetEmptyWebProxy().

using System.Net;
using System.Net.Http;

var handler = new HttpClientHandler
{
    Proxy = GlobalProxySelection.GetEmptyWebProxy()
};

using var client = new HttpClient(handler);
var response = await client.GetAsync("https://example.com");

Choose the right configuration scope

Requirement Recommended approach Reason
One client uses one proxy HttpClientHandler.Proxy Explicit and isolated configuration
All otherwise-unconfigured clients share a default HttpClient.DefaultProxy Central default for the process
Container or platform controls routing Environment variables Deployment changes do not require a rebuild
Different destinations need different proxies Multiple named clients or handlers Each handler owns its proxy and bypass rules
No proxy for a sensitive client Empty proxy Overrides ambient proxy configuration

Reuse HttpClient and its handler

Do not create and dispose a new HttpClient for every request. Each instance has its own connection pool; excessive creation can cause unnecessary connection setup and contribute to port exhaustion. Microsoft recommends long-lived clients with PooledConnectionLifetime on modern .NET, or clients created through IHttpClientFactory. Read the HttpClient guidelines for the lifetime trade-offs.

using System.Net;
using System.Net.Http;

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("http://proxy.example:8080")
};

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

// Reuse client for many requests.
for (var i = 0; i < 10; i++)
{
    using var response = await client.GetAsync("https://example.com");
    response.EnsureSuccessStatusCode();
}

If DNS changes must be observed by a long-lived client, configure an appropriate pooled connection lifetime on the underlying handler where supported by your target .NET version. If your application uses cookies, note Microsoft’s factory caveat: pooled handlers can share CookieContainer instances, and recycling a handler loses cookies stored there.

Timeouts, cancellation, and response handling

A proxy adds another network hop, so use a timeout that matches the operation and pass a cancellation token for request-level control. Always dispose responses when streaming or issuing many requests, and call EnsureSuccessStatusCode when non-success responses should fail fast.

ScreenshotNeo removes common consent banners, popups, and chat widgets before capture.
ScreenshotNeo removes common consent banners, popups, and chat widgets before capture.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var response = await client.GetAsync(
    "https://example.com/large-file",
    HttpCompletionOption.ResponseHeadersRead,
    cts.Token);

response.EnsureSuccessStatusCode();
await using var input = await response.Content.ReadAsStreamAsync(cts.Token);
await using var output = File.Create("download.bin");
await input.CopyToAsync(output, cts.Token);

Or skip the browser setup

If your goal is to obtain a clean image of a URL rather than build and operate a browser capture stack, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets each cleanup step be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed.

See the ScreenshotNeo API documentation for all options, including full-page capture with lazy-image loading, CSS element capture, dark mode, device presets, custom viewports, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture, usage, and the OpenAPI specification.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', buffer);

An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Free accounts include 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Troubleshooting checklist

The request bypasses the proxy

  • Check whether the handler has an explicit Proxy; it overrides defaults.
  • Inspect NO_PROXY, BypassList, and local bypass behavior.
  • Confirm the process sees the environment variables you configured, especially inside a container.

407 Proxy Authentication Required

The proxy received the request but rejected its credentials. Verify the username, password, authentication scheme, and whether NetworkCredential or default credentials are expected. Remove secrets from logs while diagnosing.

Proxy URI or connection errors

Check the hostname, port, firewall access, and proxy value syntax. For documented environment configuration, use an http-prefixed proxy URL without a path. Do not confuse that setting with the scheme of the destination URL.

HTTPS requests fail while HTTP works

Confirm that HTTPS_PROXY is set when relying on environment configuration, or that the handler proxy is reachable for HTTPS destinations. A proxy may require CONNECT support or policy changes for TLS destinations.

Requests time out

Test DNS resolution and TCP access to the proxy host, then review proxy load, destination response time, and your client timeout. Use cancellation tokens so stalled operations do not remain active indefinitely.

Cookies disappear with IHttpClientFactory

Factory-managed handlers are pooled and can recycle their cookie containers. If cookies are part of correctness, choose a lifetime and cookie strategy that matches the application, or manage the relevant client and handler explicitly.

Performance, reliability, and cost considerations

  • Connection reuse: Reusing a client keeps pooled connections available and avoids repeated proxy handshakes.
  • Proxy capacity: A slow or overloaded proxy limits throughput regardless of local CPU. Measure end-to-end latency and status codes in your application.
  • Retries: Retry only transient failures, use bounded backoff, and avoid duplicating non-idempotent operations.
  • Multiple proxies: Use separate clients when routing requirements differ; do not mutate a handler’s proxy while requests are in flight.
  • Secrets: Store proxy credentials outside source and redact authorization headers from logs.
  • Platform defaults: Windows, macOS, and Linux can initialize global proxy behavior differently, so document deployment assumptions.
  • Screenshot workloads: ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits cost nothing, and verdict headers make the result visible.

Frequently asked questions

Does HttpClient support SOCKS proxies?

The examples here use the built-in HTTP proxy configuration documented by Microsoft. Use a handler or library that explicitly supports the proxy protocol required by your environment.

Does setting HttpClientHandler.Proxy disable all system settings?

An explicitly configured handler proxy takes precedence for that client. Bypass rules can still send matching destinations directly.

Should I use one HttpClient for every proxy?

Use one deliberately configured client or named client per proxy policy. Reuse each client; separate instances are appropriate when applications need multiple proxies.

Can I change the proxy for an existing client?

Configure the handler before constructing the client. For a different proxy, create another configured handler and client rather than changing routing while requests are active.

Where can I find the complete ScreenshotNeo option list?

The ScreenshotNeo documentation lists capture, browser, PDF, caching, asynchronous, bulk, and delivery parameters.