9

I have an AJAX form which post a form-data to a local API url: /api/document. It contains a file and a custom Id. We simply want to take the exact received Request and forward it to a remote API at example.com:8000/document/upload.

Is there a simple way of achieve this "forward" (or proxy?) of the Request to a remote API using Asp.NET Core?

Below we had the idea to simply use Web API Http client to get the request and then resend it (by doing so we want to be able to for example append a private api key from the backend), but it seems not to work properly, the PostAsync doesn't accept the Request.

Raw request sent by Ajax

POST http://localhost:62640/api/document HTTP/1.1
Host: localhost:62640
Connection: keep-alive
Content-Length: 77424
Accept: application/json
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryn1BS5IFplQcUklyt
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,fr;q=0.6

------WebKitFormBoundaryn1BS5IFplQcUklyt
Content-Disposition: form-data; name="fileToUpload"; filename="test-document.pdf"
Content-Type: application/pdf
...
------WebKitFormBoundaryn1BS5IFplQcUklyt
Content-Disposition: form-data; name="id"

someid
------WebKitFormBoundaryn1BS5IFplQcUklyt--

Backend Code

Our .NET Core backend has a simple "forward to another API" purpose.

public class DocumentUploadResult
{
     public int errorCode;
     public string docId;
}

[Route("api/[controller]")]
public class DocumentController : Controller
{
    // POST api/document
    [HttpPost]
    public async Task<DocumentUploadResult> Post()
    {
        client.BaseAddress = new Uri("http://example.com:8000");

        client.DefaultRequestHeaders.Accept.Clear();
        HttpResponseMessage response = await client.PostAsync("/document/upload", Request.Form);
        if (response.IsSuccessStatusCode)
        {
            retValue = await response.Content.ReadAsAsync<DocumentUploadResult>();
        }
        return retValue;
    }
}

We have a GET request (not reproduced here) which works just fine. As it doesn't have to fetch data from locally POSTed data.

My question

How to simply pass the incoming local HttpPost request and forwarding it to the remote API?

I searched A LOT on stackoverflow or on the web but all are old resources talking about forwarding Request.Content to the remote.

But on Asp.NET Core 1.0, we don't have access to Content. We only are able to retrieve Request.Form (nor Request.Body) which is then not accepted as an argument of PostAsync method:

Cannot convert from Microsoft.AspNetCore.Http.IformCollection to System.Net.Http.HttpContent

I had the idea to directly pass the request to the postAsync:

Cannot convert from Microsoft.AspNetCore.Http.HttpRequest to System.Net.Http.HttpContent

I don't know how to rebuild expected HttpContent from the local request I receive.

Expected response

For information, When we post a valid form-data with the custom Id and the uploaded file, the remote (example.com) API response is:

{
  "errorCode": 0
  "docId": "585846a1afe8ad12e46a4e60"
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
BlackHoleGalaxy
  • 9,160
  • 17
  • 59
  • 103
  • 1
    If the form is a simple key value pair then you can extract the forma data and put it in a dictionary that you can then pass on to the next api – Nkosi Jan 06 '17 at 15:28
  • It's not : it also contains a file. It's a form data with both a file and `id: value`. – BlackHoleGalaxy Jan 06 '17 at 15:30
  • Create a new `HttpRequestMessage`, copy the necessary info over and send that on to the api – Nkosi Jan 06 '17 at 15:32
  • I'm looking toward this solution. But how to read the file content from the local post request? And the id? I though about using `var filecontent = new StreamContent(Request.Form.Files[0].OpenReadStream());` but the content is empty. – BlackHoleGalaxy Jan 06 '17 at 15:41
  • Check this and see if it comes close. was going to suggest something like this but checked first to see if there were any duplicates around http://stackoverflow.com/a/39416373/5233410 – Nkosi Jan 06 '17 at 16:20
  • Just noticed someone suggested the same thing on your other question – Nkosi Jan 06 '17 at 16:23
  • Thanks I'll check it out. I'm surprise there is no way to simply proxy an entire request to another uri. What's why I leave this actual question here. – BlackHoleGalaxy Jan 06 '17 at 16:25
  • quick question, is it one or multiple files being uploaded along with the viewModel? – Nkosi Jan 06 '17 at 16:37
  • Only one file. That's why I thought it would be simple to transfer the request to the other api. – BlackHoleGalaxy Jan 06 '17 at 16:58
  • You figured out your solution or do i still need to draft up an answer? confirm what the viewmodel has in it as well if any and the action url – Nkosi Jan 06 '17 at 16:59
  • I didn't figure it out... I really struggle to retrieve the simple file and id and then pass it to the remote API. I put the ajax request in the question. My form data contains only the file and an id. `Content-Disposition: form-data; name="fileToUpload"; filename="test-document.pdf" Content-Disposition: form-data; name="id"` – BlackHoleGalaxy Jan 06 '17 at 17:02
  • I'm struggling because my POST contains both a file AND a key/value pair with key = `id`. How to retrieve them both and then pass them to a new MultipartFormDataContent sent to the remote API. – BlackHoleGalaxy Jan 06 '17 at 17:11
  • create a viewmodel to hold both the property and file upload. drafting up answer now – Nkosi Jan 06 '17 at 17:12

1 Answers1

9

Ok first create a view model to hold form information. Since file upload is involved, include IFormFile in the model.

public class FormData {
    public int id { get; set; }
    public IFormFile fileToUpload { get; set; }
}

The model binder should pick up the types and populate the model with the incoming data.

Update controller action to accept the model and proxy the data forward by copying content to new request.

[Route("api/[controller]")]
public class DocumentController : Controller {
    // POST api/document
    [HttpPost]
    public async Task<IActionResult> Post(FormData formData) {
        if(formData != null && ModelState.IsValid) {
            client.BaseAddress = new Uri("http://example.com:8000");
            client.DefaultRequestHeaders.Accept.Clear();

            var multiContent = new MultipartFormDataContent();

            var file = formData.fileToUpload;
            if(file != null) {
                var fileStreamContent = new StreamContent(file.OpenReadStream());
                multiContent.Add(fileStreamContent, "fileToUpload", file.FileName);
            }

            multiContent.Add(new StringContent(formData.id.ToString()), "id");

            var response = await client.PostAsync("/document/upload", multiContent);
            if (response.IsSuccessStatusCode) {
               var retValue = await response.Content.ReadAsAsync<DocumentUploadResult>();
               return Ok(reyValue);
            }
        }
        //if we get this far something Failed.
        return BadRequest();
    }        
}

You can include the necessary exception handlers as needed but this is a minimal example of how to pass the form data forward.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks Nkosi! Will try it quickly. Talking about exception handling in `Httpclient` requests, do you know any good documentation? – BlackHoleGalaxy Jan 06 '17 at 19:23
  • 1
    Error handling - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling – Nkosi Jan 06 '17 at 19:25
  • 1
    About the line `if(model != null && ModelState.IsValid) {`what `model` refers to? It seems not being defined. Is this a typo and I have to enter `data`? – BlackHoleGalaxy Jan 06 '17 at 20:36
  • @BlackHoleGalaxy Yes typo on my part. Thanks for the edit. – Nkosi Jan 06 '17 at 22:10