ASP.NET Core – make your own proxy

The proxy WebApi can forward client’s request to another WebApi, it is a perfect way to do 1. API management 2. API gateway for micro-services

There are two approaches to create a proxy with ASP.NET Core, 1. global middleware to capture all request and forward it to proxied WebApi url. 2. create a controller and in controller level handle the request and forward it to proxied WebApi url.

No matter which approach we use, there are some crucial logic need to be solved, how to replicate raw request include query string and payload body; how to capture the response from proxied WebApi; how to return it back to client.

public static class RouteProxyExtensions
{
    public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, string uriString, bool isAuthorizeProxied)
    {
        var uri = new Uri(uriString);
        var request = context.Request;
        var requestMessage = new HttpRequestMessage();
        var requestMethod = request.Method;

        if (!HttpMethods.IsGet(requestMethod) &&
            !HttpMethods.IsHead(requestMethod) &&
            !HttpMethods.IsDelete(requestMethod) &&
            !HttpMethods.IsTrace(requestMethod))
        {
            request.EnableRewind();
            request.Body.Seek(0, SeekOrigin.Begin);
            var streamContent = new StreamContent(request.Body);
            requestMessage.Content = streamContent;
        }

        foreach (var header in request.Headers)
        {
            if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
            {
                requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
            }
        }

        if (!isAuthorizeProxied)
        {
            requestMessage.Headers.Remove("Authorization");
        }

        requestMessage.Headers.Host = uri.Authority;
        requestMessage.RequestUri = uri;
        requestMessage.Method = new HttpMethod(request.Method);

        return requestMessage;
    }

    public static Task<HttpResponseMessage> SendProxyHttpRequest(this HttpContext context, string proxiedAddress, bool isAuthorizeProxied)
    {
        var proxiedRequest = context.CreateProxyHttpRequest(proxiedAddress, isAuthorizeProxied);
        return new HttpClient().SendAsync(proxiedRequest, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
    }

    public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
    {
        var response = context.Response;

        response.StatusCode = (int)responseMessage.StatusCode;

        foreach (var header in responseMessage.Headers)
        {
            response.Headers[header.Key] = header.Value.ToArray();
        }

        foreach (var header in responseMessage.Content.Headers)
        {
            response.Headers[header.Key] = header.Value.ToArray();
        }

        response.Headers.Remove("transfer-encoding");

        using (var responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
        {
            await responseStream.CopyToAsync(response.Body, 81920, context.RequestAborted).ConfigureAwait(false);
        }
    }
}

CreateProxyHttpRequest method can replicate raw request of client call, SendProxyHttpRequest can send replicated request to proxied WebApi, CopyProxyHttpResponse can capture the response from proxied WebApi and send it back to client.

Now by solving the critical problems, no matter global middleware approach or controller approach, you just need to use those logic to replicate request and forward it to next proxied WebApi url and write the response back to the same request.

for example:

var proxiedResponse = await context.HttpContext.SendProxyHttpRequest(forwardUrl, IsAuthorizeProxied);
await context.HttpContext.CopyProxyHttpResponse(proxiedResponse);

There is one thing you need to notice, when replicate raw request body, you need to call request.EnableRewind() to make sure it could be re-use, then need to call request.Body.Seek(0, SeekOrigin.Begin) to make sure the replicate logic can get the whole stream from beginning; otherwise, you may will get exception of cannot read the request.Body stream.

Furthermore, you can wrap the whole logic to ActionFilterAttribute, and just mark the controller class or method with this attribute:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class RouteProxyAttribute : ActionFilterAttribute
{
    public bool IsAuthorizeProxied { get; set; }
    public string ForwardUrl { get; set; }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var proxiedResponse = await context.HttpContext.SendProxyHttpRequest(ForwardUrl, IsAuthorizeProxied);
        await context.HttpContext.CopyProxyHttpResponse(proxiedResponse);
    }
}

and in controller, mark it in method or controller class:

[HttpGet]
[RouteProxy(ForwardUrl="http://xxx/api/debug")]
public void Get()
{
}

or

[Route("api/[controller]")]
[RouteProxy]
public class DebugController : Controller
{
}

Leave a comment