Skip to main content

Command Palette

Search for a command to run...

Configuring HTTP Proxy for gRPC in C# Without Environment Variables

Updated
3 min read
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:

FilePurpose
http_proxy.ccReads http_proxy env var, sets channel args
http_connect_handshaker.ccSends HTTP CONNECT request to proxy
secure_channel_create.ccSSL/TLS target name override handling
channel.ccDefault authority header logic
ChannelOptions.csC# 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:

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.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:

: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?

ApproachScopeRisk
http_proxy env varGlobal (all HTTP)May break other services
Channel OptionsPer-channelIsolated, 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