Configuring HTTP Proxy for gRPC in C# Without Environment Variables

The Challenge
You have a gRPC client in C# using Grpc.Core that needs to route traffic through an HTTP proxy. Sounds simple, right?
Not quite.
If you've searched for solutions, you've probably found:
Set
http_proxyenvironment variable ✅ Works, but affects ALL HTTP trafficUse
Grpc.Net.ClientwithHttpClientHandler.Proxy✅ Clean, but requires library migrationSet
grpc.http_proxychannel option ❌ Doesn't work in Grpc.Core
I needed per-channel proxy configuration without affecting other traffic and without migrating libraries. So I dove into the gRPC C-core source code to understand how http_proxy actually works.
My major limitation was that my monolith library is using grpc.core v1.10.0 and had no options such as grpc_proxy
Source Code References (gRPC v1.10.0)
If you want to explore the internals yourself, here are the key files:
| File | Purpose |
http_proxy.cc | Reads http_proxy env var, sets channel args |
http_connect_handshaker.cc | Sends HTTP CONNECT request to proxy |
secure_channel_create.cc | SSL/TLS target name override handling |
channel.cc | Default authority header logic |
ChannelOptions.cs | C# channel option constants |
What I Discovered
When gRPC honors the http_proxy environment variable, it doesn't do anything magical. It simply:
Parses the proxy URL
Sets internal channel arguments
Uses these arguments during connection
The key insight: these channel arguments are accessible via ChannelOption in C#!
The Solution
Understanding HTTP CONNECT Tunneling
HTTP proxies use the CONNECT method to create TCP tunnels:

Once the tunnel is established, TLS handshake and gRPC communication flow through transparently.
The Three Magic Channel Options
private Channel CreateChannel(string targetEndpoint, SslCredentials credentials, string proxyEndpoint)
{
string proxy = proxyEndpoint?.Trim();
if (!string.IsNullOrEmpty(proxy))
{
// Extract hostname without port for SSL validation
string targetHost = targetEndpoint.Contains(":")
? targetEndpoint.Substring(0, targetEndpoint.LastIndexOf(':'))
: targetEndpoint;
var options = new[]
{
// 1. HTTP CONNECT tunnel target (host:port)
new ChannelOption("grpc.http_connect_server", targetEndpoint),
// 2. SSL SNI + certificate validation (hostname only)
new ChannelOption(ChannelOptions.SslTargetNameOverride, targetHost),
// 3. HTTP/2 :authority header (host:port)
new ChannelOption(ChannelOptions.DefaultAuthority, targetEndpoint)
};
// Channel connects to PROXY, tunnels to TARGET
return new Channel(proxy, credentials, options);
}
// Direct connection
return new Channel(targetEndpoint, credentials);
}
What Each Option Does
1. grpc.http_connect_server
Purpose: Tells gRPC where to tunnel
What happens: When gRPC connects to the channel target (the proxy), it sends:
CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443
Format: host:port (port is required!)
2. SslTargetNameOverride
Purpose: SSL/TLS hostname for SNI and certificate validation
The problem: Without this, gRPC would:
Send SNI for the proxy hostname
Validate the certificate against the proxy hostname
Both are WRONG — we need the actual target's certificate!
What happens:
TLS ClientHello contains correct SNI:
api.example.comCertificate validation checks against:
api.example.com
Format: hostname (NO port — SSL certificates don't include ports)
3. DefaultAuthority
Purpose: Sets the HTTP/2 :authority pseudo-header
The problem: gRPC servers use :authority for routing. Without this override, it would be set to the proxy address.
What happens:
:method: POST
:scheme: https
:authority: api.example.com:443 ← Correct!
:path: /mypackage.MyService/MyMethod
Format: host:port (port often required for server routing)
Complete Sequence Diagram

Why Not Just Use Environment Variables?
| Approach | Scope | Risk |
http_proxy env var | Global (all HTTP) | May break other services |
| Channel Options | Per-channel | Isolated, controlled |
Environment variables are global. If your application makes HTTP calls to multiple services, and only ONE needs proxying, you can't use environment variables safely.
Channel options give you surgical precision.
By understanding how gRPC handles proxies internally, we can configure per-channel proxy support without environment variables, keeping our other HTTP traffic unaffected.Configuring HTTP Proxy for gRPC in C# Without Environment Variables

