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
};
}
}