C#
proxy development
programming tutorial
networking
software engineering

How to create a simple proxy in C?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A simple HTTP proxy receives a request from a client, forwards a matching request to another server, and sends the upstream response back to the caller. In C#, the easiest learning path is usually a small reverse proxy built with HttpListener and HttpClient.

Build A Minimal Forwarding Proxy

The example below listens on http://localhost:8080/, forwards requests to a fixed upstream host, and returns the upstream response. It is intentionally small so the request flow is easy to follow.

csharp
1using System;
2using System.Net;
3using System.Net.Http;
4using System.Threading.Tasks;
5
6class Program
7{
8    static async Task Main()
9    {
10        var listener = new HttpListener();
11        listener.Prefixes.Add("http://localhost:8080/");
12        listener.Start();
13
14        using var httpClient = new HttpClient
15        {
16            Timeout = TimeSpan.FromSeconds(15)
17        };
18
19        Console.WriteLine("Proxy listening on http://localhost:8080/");
20
21        while (true)
22        {
23            var context = await listener.GetContextAsync();
24            _ = Task.Run(() => HandleRequestAsync(context, httpClient));
25        }
26    }
27
28    static async Task HandleRequestAsync(HttpListenerContext context, HttpClient httpClient)
29    {
30        var upstreamUri = new Uri("https://example.com" + context.Request.RawUrl);
31        using var upstreamRequest = new HttpRequestMessage(
32            new HttpMethod(context.Request.HttpMethod),
33            upstreamUri);
34
35        using var upstreamResponse = await httpClient.SendAsync(
36            upstreamRequest,
37            HttpCompletionOption.ResponseHeadersRead);
38
39        context.Response.StatusCode = (int)upstreamResponse.StatusCode;
40
41        await using var responseStream = await upstreamResponse.Content.ReadAsStreamAsync();
42        await responseStream.CopyToAsync(context.Response.OutputStream);
43        context.Response.Close();
44    }
45}

This handles basic GET requests well enough for experimentation, debugging, or learning. It is not a complete production proxy, but it shows the essential control flow clearly.

Forward Bodies And Selected Headers

Real proxies need to forward more than the path. For POST, PUT, and PATCH, you also need the request body and important headers.

csharp
1static async Task HandleRequestAsync(HttpListenerContext context, HttpClient httpClient)
2{
3    var upstreamUri = new Uri("https://example.com" + context.Request.RawUrl);
4    using var upstreamRequest = new HttpRequestMessage(
5        new HttpMethod(context.Request.HttpMethod),
6        upstreamUri);
7
8    if (context.Request.HasEntityBody)
9    {
10        upstreamRequest.Content = new StreamContent(context.Request.InputStream);
11        if (!string.IsNullOrWhiteSpace(context.Request.ContentType))
12        {
13            upstreamRequest.Content.Headers.TryAddWithoutValidation(
14                "Content-Type",
15                context.Request.ContentType);
16        }
17    }
18
19    if (!string.IsNullOrWhiteSpace(context.Request.UserAgent))
20    {
21        upstreamRequest.Headers.TryAddWithoutValidation("User-Agent", context.Request.UserAgent);
22    }
23
24    using var upstreamResponse = await httpClient.SendAsync(upstreamRequest);
25    context.Response.StatusCode = (int)upstreamResponse.StatusCode;
26
27    await using var responseStream = await upstreamResponse.Content.ReadAsStreamAsync();
28    await responseStream.CopyToAsync(context.Response.OutputStream);
29    context.Response.Close();
30}

Forwarding every header blindly is a mistake. Some headers are connection-specific and belong to only one hop in the chain. A simple teaching proxy should copy only the pieces it actually needs.

Add Error Handling Early

A proxy sits between two unreliable systems, so network errors are normal rather than exceptional. Timeouts and upstream failures should become useful HTTP responses.

csharp
1try
2{
3    using var upstreamResponse = await httpClient.SendAsync(upstreamRequest);
4    context.Response.StatusCode = (int)upstreamResponse.StatusCode;
5    await using var responseStream = await upstreamResponse.Content.ReadAsStreamAsync();
6    await responseStream.CopyToAsync(context.Response.OutputStream);
7}
8catch (TaskCanceledException)
9{
10    context.Response.StatusCode = (int)HttpStatusCode.GatewayTimeout;
11}
12catch (HttpRequestException)
13{
14    context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
15}
16finally
17{
18    context.Response.Close();
19}

Returning 502 or 504 is much better than letting the client see a closed socket with no explanation.

Understand The Boundaries Of A Simple Proxy

The simple version is good for local debugging, API inspection, or learning. It is not ready for internet-facing production traffic. Production proxies usually need:

  • HTTPS CONNECT support or TLS termination
  • request size limits
  • better cancellation and timeout behavior
  • authentication and access control
  • logging and observability
  • protection against header spoofing and request smuggling

Those are not optional extras. They are part of what makes a proxy safe and correct in the real world.

That does not make the minimal version useless. It simply means its role is educational. Once you understand request in, upstream request out, response back, the next improvements are much easier to reason about.

Common Pitfalls

  • Creating a new HttpClient for every request instead of reusing one instance.
  • Forgetting to forward request bodies for methods other than GET.
  • Copying all incoming headers without filtering out connection-specific ones.
  • Calling the result a full proxy when it does not support HTTPS tunneling or serious security controls.
  • Ignoring timeouts, DNS failures, and upstream errors.

Summary

  • A small C# proxy can be built with HttpListener and a shared HttpClient.
  • Forward method, path, body, and only the headers that make sense for end-to-end forwarding.
  • Translate upstream failures into useful HTTP status codes.
  • Treat a minimal proxy as a learning tool, not as a production-ready edge service.

Course illustration
Course illustration