7

Part of my application needs to act as a Proxy Server for a third party RESTful web service. Is there a way to set up Web API routing so that all requests of the same type will go to the same method?

For example, if the client sends in either of these GET requests I want them to go into a single GET action method that then sends on the request to the downstream server.

api/Proxy/Customers/10045
api/Proxy/Customers/10045/orders
api/Proxy/Customers?lastname=smith

The single action method for GET would pick up any one of these three requests and send them on to the respective service (I know how to work with HttpClient to make that happen effectively):

http://otherwebservice.com/Customers/10045
http://otherwebservice.com/Customers/10045/orders
http://otherwebservice.com/Customers?lastname=smith

I don't want to have to tightly couple my web service to the third party web service and replicate their entire API as method calls inside mine.

One workaround that I have thought of is to simply encode the target URL in JavaScript on the client and pass this into the Web API which will then only see one parameter. It would work, but I'd prefer to use the routing capabilities in Web API if possible.

Graham
  • 7,431
  • 18
  • 59
  • 84

1 Answers1

18

Here's how I got this to work. First, create a controller with a method for each verb you want to support:

public class ProxyController : ApiController
{
    private Uri _baseUri = new Uri("http://otherwebservice.com");

    public async Task<HttpResponseMessage> Get(string url)
    {
    }

    public async Task<HttpResponseMessage> Post(string url)
    {
    }

    public async Task<HttpResponseMessage> Put(string url)
    {
    }

    public async Task<HttpResponseMessage> Delete(string url)
    {
    }
}

The methods are async because they're going to use an HttpClient. Map your route like this:

config.Routes.MapHttpRoute(
  name: "Proxy",
  routeTemplate: "api/Proxy/{*url}",
  defaults: new { controller = "Proxy" });

Now back to the Get method in the controller. Create an HttpClient object, create a new HttpRequestMessage object with the appropriate Url, copy everything (or almost everything) from the original request message, then call SendAsync():

public async Task<HttpResponseMessage> Get(string url)
{
    using (var httpClient = new HttpClient())         
    {
        string absoluteUrl = _baseUri.ToString() + "/" + url + Request.RequestUri.Query;
        var proxyRequest = new HttpRequestMessage(Request.Method, absoluteUrl);
        foreach (var header in Request.Headers)
        {
            proxyRequest.Headers.Add(header.Key, header.Value);
        }

        return await httpClient.SendAsync(proxyRequest, HttpCompletionOption.ResponseContentRead);
    }
}

The URL combining could be more sophisticated, but that's the basic idea. For the Post and Put methods, you'll also need to copy the request body

Also please note a HttpCompletionOption.ResponseContentRead parameter passed in SendAsync call, because without it, ASP.NET will spend an exremeley long time reading the content if the content is large (in my case, it changed a 500KB 100ms request into a 60s request).

stop-cran
  • 4,229
  • 2
  • 30
  • 47
Eric Pohl
  • 2,324
  • 1
  • 21
  • 31
  • Hmmm... doesn't seem to work for query parameters. Still working on that. – Eric Pohl Aug 21 '14 at 14:52
  • Had a hard time implementing this... The reason was the copying of the headers. As soon as I removed the foreach, everything worked fine. – NoRyb Mar 01 '16 at 10:34
  • @NoRyb What was it about copying the headers that gave you trouble? – Eric Pohl Mar 01 '16 at 19:08
  • @NoRyb My only problem came from the Host-header with TSL when relaying to another domain. When I left that uncopied, everything worked fine. What problem did you run into? – Tuukka Haapaniemi Sep 14 '16 at 14:12
  • @TuukkaHaapaniemi @EricPohl I tried to reproduce the problem (I had to work around it so I'm not sure anymore if it was this) but this time I got the exception `Could not establish a trust relationship for the SSL/TLS secure channel`. It might be related to your comment. I filtered out the header.Key `Host` and didn't get the exception anymore. I guess that's the solution - but is it "correct" to do that? Are there any other headers that should be filtered out? – NoRyb Sep 15 '16 at 09:40
  • So I have an internally hosted API that I would like to use externally (http://stackoverflow.com/questions/43002283/how-to-allow-internal-mvc-web-api-from-external-site-outside-the-network). How would the proxy work for my purpose? – Si8 Mar 24 '17 at 16:15
  • @Si8 The proxy controller is assuming that the URL structures are basically the same between the internal and external APIs. Assuming your system is not completely open, you'll want to think carefully about security. Make sure to authorize the incoming request, and figure out how to send the proper auth header (which could be the same or different depending on your setup) to the internal API. – Eric Pohl Mar 24 '17 at 21:30
  • Thank you. Can you guide me the way for my api please? – Si8 Mar 26 '17 at 09:55
  • 1
    Be careful if the request contains a Host header. It appears that the HttpClient will replace the base part of the Uri with the contents of the host header. This can result in the http call being directed another server than where you requested it to. – user957902 Jul 27 '17 at 15:17
  • I made an edit that I thought was the solution to my problem, but I had found out that my issue was due to some other problem and that setting ContentRead was coincidental (in fact, I had later found out that ContentRead is the default and thus does not need to be provided and thus has no effect). I tried reverting my edit but it got rejected (so feel free to revert it). Though, I was able to get it to perform much better (aside from my issue) by using a combination of stream content and HeadersRead. – Andrew Larsson Mar 29 '18 at 23:22
  • I'm getting `ExceptionMessage": "The request was aborted: Could not create SSL/TLS secure channel."` errors. – Cole Jul 17 '18 at 13:15