# 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_proxy` environment variable ✅ Works, but affects ALL HTTP traffic
    
* Use `Grpc.Net.Client` with `HttpClientHandler.Proxy` ✅ Clean, but requires library migration
    
* Set `grpc.http_proxy` channel 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`](https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/filters/client_channel/http_proxy.cc) | Reads `http_proxy` env var, sets channel args |
| [`http_connect_handshaker.cc`](https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/filters/client_channel/http_connect_handshaker.cc) | Sends HTTP CONNECT request to proxy |
| [`secure_channel_create.cc`](https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/transport/chttp2/client/secure/secure_channel_create.cc) | SSL/TLS target name override handling |
| [`channel.cc`](https://github.com/grpc/grpc/blob/v1.10.0/src/core/lib/surface/channel.cc) | Default authority header logic |
| [`ChannelOptions.cs`](https://github.com/grpc/grpc/blob/v1.10.0/src/csharp/Grpc.Core/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:

1. Parses the proxy URL
    
2. Sets internal channel arguments
    
3. 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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765631349932/d6a93113-8002-4b9b-801e-62b41358718a.png align="center")

Once the tunnel is established, TLS handshake and gRPC communication flow through transparently.

### **The Three Magic Channel Options**

```csharp
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:

```bash
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.com`
    
* Certificate 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:**

```plaintext
: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**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765631407611/72b22b30-f6a8-4420-8c1a-8eec99b81b74.png align="center")

## **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**
