Create a custom proxy endpoint action with ASP.NET Core MVC

Create a custom proxy endpoint action with ASP.NET Core MVC

There are various reasons why it is useful to channel certain requests through your own application to an external endpoint; the most obvious is, for example, as a workaround for client statistics from browser adblockers like Microsoft Clarity or Cloudflare Web Analytics.

And you don't always need an entire proxy tool or proxy framework like YARP with huge effort and overhead. It can be much easier to do it directly in the application, for example with ASP.NET Core MVC.

A custom proxy

A proxy is nothing more than an mediator that forwards a request - either completely or slightly adapted, depending on the situation and requirements.

As a rule for forwarding:

  • The headers (except for security-relevant headers such as cookies)
  • The HTTP method
  • The content and the content type
  • Sometimes the IP address of the source, if necessary or desired

ASP.NET Core does not have a built-in mechanism for this, but you can easily implement this yourself, both with MVC and with the Minimal API.

As a demonstration, I have an ASP.NET Core MVC action that receives a request and can forward it to any destination address. The result is the respective status code and content of the external response.

// Controller to handle proxy requests
public class SampleProxyController : Controller
{
    // Dependency injection for IHttpClientFactory to create HttpClient instances
    private readonly IHttpClientFactory _httpClientFactory;

    // Constructor to initialize the IHttpClientFactory instance
    public SampleProxyController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    // Action method that proxies the request
    public async Task<IActionResult> Proxy()
    {
        // The target proxy URI. This can be dynamically set or statically defined.
        Uri proxyUri = new("https://myproxyurl/endpoint"); // your config

        // Enforce the HTTP version for the request to HTTP/2
        Version proxyHttpVersion = new("2.0"); // your config

        // Get the incoming request from the client
        HttpRequest sourceRequest = this.Request;

        // Extract the query string from the source request, if any
        string? queryString = sourceRequest.QueryString.Value ?? null;

        // Build the target URI, appending the query string if it exists
        UriBuilder uriBuilder = new(proxyUri);
        if (queryString is not null)
        {
            uriBuilder.Query = queryString;
        }
        Uri targetUri = uriBuilder.Uri;

        // Create a new HTTP request message for the proxy target
        HttpRequestMessage proxyRequestMessage = new()
        {
            RequestUri = targetUri,
            Method = new HttpMethod(sourceRequest.Method), // Use the HTTP method from the source request
            Version = proxyHttpVersion
        };

        // Forward headers from the source request, except the "Cookie" header
        foreach (KeyValuePair<string, StringValues> header in sourceRequest.Headers)
        {
            if (!string.Equals(header.Key, "Cookie", StringComparison.OrdinalIgnoreCase))
            {
                proxyRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToList());
            }
        }

        // Set the Host header to match the target URI's authority
        proxyRequestMessage.Headers.Host = targetUri.Authority;

        // Add the X-Forwarded-For header to indicate the client's IP address
        IPAddress? requestIPAddress = sourceRequest.HttpContext.Connection.RemoteIpAddress;
        proxyRequestMessage.Headers.TryAddWithoutValidation("X-Forwarded-For", requestIPAddress?.ToString());

        // Forward the body of the source request to the proxy target
        proxyRequestMessage.Content = new StreamContent(sourceRequest.Body);
        if (sourceRequest.Headers.TryGetValue("Content-Type", out StringValues value))
        {
            string? contentType = value;
            if (contentType is not null)
            {
                proxyRequestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
            }
        }

        // Create an HttpClient instance using the factory and send the proxy request
        HttpClient httpClient = _httpClientFactory.CreateClient("YourClientProxyNameHere"); // replace this with your config
        HttpResponseMessage proxyResponse = await httpClient.SendAsync(proxyRequestMessage).ConfigureAwait(false);

        // Add CORS headers to allow cross-origin requests if required
        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Origin", sourceRequest.Headers.Origin.FirstOrDefault() ?? "*");
        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Headers", "content-type");
        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Credentials", "true");

        // Read the response content and its type from the proxy response
        string proxyResponseContent = await proxyResponse.Content.ReadAsStringAsync();
        string? proxyResponseContentType = proxyResponse.Content.Headers.ContentType?.ToString();

        // Return the response from the proxy target as the result of this action
        return new ContentResult
        {
            StatusCode = (int)proxyResponse.StatusCode, // Use the same status code as the proxy response
            Content = proxyResponseContent, // Forward the response content
            ContentType = proxyResponseContentType // Set the content type
        };
    }
}