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.

 1// Controller to handle proxy requests
 2public class SampleProxyController : Controller
 3{
 4    // Dependency injection for IHttpClientFactory to create HttpClient instances
 5    private readonly IHttpClientFactory _httpClientFactory;
 6
 7    // Constructor to initialize the IHttpClientFactory instance
 8    public SampleProxyController(IHttpClientFactory httpClientFactory)
 9    {
10        _httpClientFactory = httpClientFactory;
11    }
12
13    // Action method that proxies the request
14    public async Task<IActionResult> Proxy()
15    {
16        // The target proxy URI. This can be dynamically set or statically defined.
17        Uri proxyUri = new("https://myproxyurl/endpoint"); // your config
18
19        // Enforce the HTTP version for the request to HTTP/2
20        Version proxyHttpVersion = new("2.0"); // your config
21
22        // Get the incoming request from the client
23        HttpRequest sourceRequest = this.Request;
24
25        // Extract the query string from the source request, if any
26        string? queryString = sourceRequest.QueryString.Value ?? null;
27
28        // Build the target URI, appending the query string if it exists
29        UriBuilder uriBuilder = new(proxyUri);
30        if (queryString is not null)
31        {
32            uriBuilder.Query = queryString;
33        }
34        Uri targetUri = uriBuilder.Uri;
35
36        // Create a new HTTP request message for the proxy target
37        HttpRequestMessage proxyRequestMessage = new()
38        {
39            RequestUri = targetUri,
40            Method = new HttpMethod(sourceRequest.Method), // Use the HTTP method from the source request
41            Version = proxyHttpVersion
42        };
43
44        // Forward headers from the source request, except the "Cookie" header
45        foreach (KeyValuePair<string, StringValues> header in sourceRequest.Headers)
46        {
47            if (!string.Equals(header.Key, "Cookie", StringComparison.OrdinalIgnoreCase))
48            {
49                proxyRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToList());
50            }
51        }
52
53        // Set the Host header to match the target URI's authority
54        proxyRequestMessage.Headers.Host = targetUri.Authority;
55
56        // Add the X-Forwarded-For header to indicate the client's IP address
57        IPAddress? requestIPAddress = sourceRequest.HttpContext.Connection.RemoteIpAddress;
58        proxyRequestMessage.Headers.TryAddWithoutValidation("X-Forwarded-For", requestIPAddress?.ToString());
59
60        // Forward the body of the source request to the proxy target
61        proxyRequestMessage.Content = new StreamContent(sourceRequest.Body);
62        if (sourceRequest.Headers.TryGetValue("Content-Type", out StringValues value))
63        {
64            string? contentType = value;
65            if (contentType is not null)
66            {
67                proxyRequestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
68            }
69        }
70
71        // Create an HttpClient instance using the factory and send the proxy request
72        HttpClient httpClient = _httpClientFactory.CreateClient("YourClientProxyNameHere"); // replace this with your config
73        HttpResponseMessage proxyResponse = await httpClient.SendAsync(proxyRequestMessage).ConfigureAwait(false);
74
75        // Add CORS headers to allow cross-origin requests if required
76        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Origin", sourceRequest.Headers.Origin.FirstOrDefault() ?? "*");
77        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Headers", "content-type");
78        sourceRequest.HttpContext.Response.Headers.AddIfNotExists("Access-Control-Allow-Credentials", "true");
79
80        // Read the response content and its type from the proxy response
81        string proxyResponseContent = await proxyResponse.Content.ReadAsStringAsync();
82        string? proxyResponseContentType = proxyResponse.Content.Headers.ContentType?.ToString();
83
84        // Return the response from the proxy target as the result of this action
85        return new ContentResult
86        {
87            StatusCode = (int)proxyResponse.StatusCode, // Use the same status code as the proxy response
88            Content = proxyResponseContent, // Forward the response content
89            ContentType = proxyResponseContentType // Set the content type
90        };
91    }
92}

Comments

Twitter Facebook LinkedIn WhatsApp