6

I'm researching a web proxy pattern that makes use of ASP.Net Core MVC project type. I'd like to basically pass a HttpRequestMessage to a httpClient in the controller which then makes a request to a remote website (like https://www.abc.fake) and then returns the response exactly as it came back from the client (body and headers). Example code:

    [Microsoft.AspNetCore.Mvc.Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    //[HttpGet]
    public async Task<HttpResponseMessage> Get()
    {
        var httpClient = new HttpClient();
        
         var resp = await httpClient.GetAsync("https://www.abc.fake");

        return resp;
        
    }
}

The problem is every time I do this I just get a JSON serialized version of the response message. It's not actually sending back the HTML I get from the remote website. This is what I'm seeing:

{"version":{"major":1,"minor":1,"build":-1,"revision":-1,"majorRevision":-1,"minorRevision":-1},"content":{"headers":[{"Key":"Content-Type","Value":["text/html; charset=utf-8"]},{"Key":"Content-Length","Value":["1036119"]},{"Key":"Expires","Value":["Wed, 23 Sep 2020 21:44:35 GMT"]},{"Key":"Last-Modified","Value":["Wed, 23 Sep 2020 21:44:35 GMT"]}]},"statusCode":200,"reasonPhrase":"OK","headers":[{"Key":"Connection","Value":["keep-alive"]},{"Key":"Vary","Value":["Accept-Encoding","Accept-Encoding"]},{"Key":"Date","Value":["Wed, 23 Sep 2020 21:41:54 GMT"]},{"Key":"Server","Value":["nginx/1.16.1"]},{"Key":"Via","Value":["1.1 varnish-v4","1.1 e8afb729a4bc6f5676d32307ea14bdae.cloudfront.fake (CloudFront)"]},{"Key":"Accept-Ranges","Value":["bytes"]},{"Key":"Cache-Control","Value":["must-revalidate, max-age=0"]},{"Key":"Set-Cookie","Value":["SWID=0C8B6C96-3F05-43D5-C3D1-2676E1C15F8C; path=/; Expires=Sun, 23 Sep 2040 21:41:54 GMT; domain=abc.fake;"]},{"Key":"X-Cache","Value":["Miss from cloudfront"]},{"Key":"X-Amz-Cf-Pop","Value":["HIO50-C1"]},{"Key":"X-Amz-Cf-Id","Value":["yKz-d9KhZdb-5qdDpppD0jeFqYHfFQA4Z1RT98Nk31eaH7kB_FXisQ=="]}],"trailingHeaders":[],"requestMessage":{"version":{"major":1,"minor":1,"build":-1,"revision":-1,"majorRevision":-1,"minorRevision":-1},"content":null,"method":{"method":"GET"},"requestUri":"https://abc.fake/","headers":[{"Key":"Request-Id","Value":["|8e9d36f9-4b9e69ca8ec31ee9.1."]}],"properties":{}},"isSuccessStatusCode":true}
15
  • Also to clarify I want to return the body AND headers from the remote website. I basically want to return the entire HttpResponseMessage as it comes back from the client. Commented Sep 23, 2020 at 21:54
  • What are you doing with HttpResponseMessage to get to the response that gives you a json string? I'd think HttpResponseMessage.Content.ReadAsByteArrayAsync() would you get you the raw response. You'd need to then use Encoding.GetString to get plain text. And it probably wouldn't include all the headers. Commented Sep 23, 2020 at 21:58
  • What is currently happening is the default serialization behavior is looking at the HttpResponseMessage and just serializing it to JSON then sending that back in the MVC controller response. That behavior just serializes all the readable properties (like http headers), this is not the behavior I want. I actually want to return all the headers I get from the remote call, in addition to the body (i.e. Content.ReadAsByteArrayAsyn()). I basically want to hand back the response I get from the remote service to the caller of the MVC service (this is a basic proxy pattern). Commented Sep 23, 2020 at 22:01
  • why dont you want to use custom classes instead of HttpResponseMessage? Not sure if it intended to be serializable. Also better to use http client factory. Commented Sep 23, 2020 at 22:11
  • @tym32167 the reason I don't want to use a custom class is because I want to use this pattern for unknown types of requests and responses. I tried to simplify my example to a single website, but I actually want to proxy all requests (think of a * route) through this one method which will send them on to various backends (based on the Host header), then I need to send back the response exactly as it comes back from the backend host. Commented Sep 23, 2020 at 22:20

3 Answers 3

3

I'm a bit late to this party, but it's worth mentioning that this mapping was very common in the early days for interop between existing Web API codebases transitioning to ASP.NET Core. It's no longer maintained, but there was a Web API Shim library written by the ASP.NET team to support the transition. Within that repo, there are 2 key files you can grab:

To wire it up, you just need to add the output formatter like this:

mvcOptions.OutputFormatters.Insert(0, new HttpResponseMessageOutputFormatter());

Using it is the same as it was back in the Web API model like this:

[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    readonly IHttpClientFactory factory;

    public WeatherForecastController(IHttpClientFactory factory) => this.factory = factory;

    [HttpGet]
    public async Task<IActionResult> Get(CancellationToken cancellationToken)
    {
        using var client = factory.CreateClient("WeatherChannel");
        var response = await client.GetAsync("https://weather.com/", cancellationToken);
        return new ResponseMessageResult(response);
    }
}

Since you mentioned you are creating a proxy, you can also build a HttpRequestMesage from the incoming HttpRequest if you need to forward it on to the HttpClient. The code is a bit more involved, but you can get everything you need from the WebApiCompatShim source.

Sign up to request clarification or add additional context in comments.

1 Comment

Using your solution in combination with creating my own attribute to use over my specific controller's actions found here made it work : stackoverflow.com/questions/73319802/…
2

After following the link to AspNetCore.Proxy nuget package that @tym32167 referred me to; I can verify that does what I wanted. For anyone that wants to do this, it's basically this simple:

[Route("{*everything}")]
public async Task Get()
{
    var host = Request.Host.Host;
    string path = Request.Path;
    string queryString = null;
    if (Request.QueryString != null)
    {
        var queryStringBuilder = new StringBuilder();
        var isFirstParameter = true;
        foreach (var parameter in Request.Query)
        {
            var leadingCharacter = isFirstParameter ? "?" : "&";
            queryStringBuilder.Append($"{leadingCharacter}{parameter.Key}={parameter.Value}");
            isFirstParameter = false;
        }
        queryString = queryStringBuilder.ToString();
    }
    var requestUrl = $"{Request.Scheme}://{host}{path}{queryString}";
    var b = HttpProxyOptionsBuilder.Instance.New();
    b.WithHttpClientName("default");
    b.WithShouldAddForwardedHeaders(true);
    var options = b.Build();         
    await this.HttpProxyAsync(requestUrl, options);
}

1 Comment

you might want to use QueryBuilder instead. Also not sure if that issue, but I was always concerning about building query manually - you might have issues with URL encoding.
0

Try something like this

public async Task<IActionResult> Get()
{
     HttpClient httpClient = new HttpClient();
     var resp = await httpClient.GetAsync("https://www.abc.fake");
     if (!resp.IsSuccessStatusCode)
         return BadRequest();

     return resp.Content;
    
}

4 Comments

I also want to get all the HTTP response headers. So am hoping for a solution where I can literally just return the full HttpResponseMessage from the client call back through to the MVC controller response. If the remote server returns a HTTP 500, 404, 302, etc. I want to return that to the caller. I basically want to proxy everything back.
Rather than down vote, may I suggest that you update your answer to use await. You should never use GetAwaiter().GetResult() in an async method than can be properly awaited.
I dont know why I did that, Fixed the code
This worked for me but needed resp.Content.ReadAsStream();

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.