I am trying to write an API endpoint that would allow a user to send a file to my server using Asp.NET Core 3.1.
I have the following action method that needs to respond to the client's request and process the file validation and store the file.
[HttpPost, Route("api/store")]
public async Task<IActionResult> Store([FromBody] MultipartFormDataContent content)
{
// Validate the file, store it.
return Ok();
}
I am trying to send the file from the client-side like this
using HttpClient client = new HttpClient();
var multiForm = new MultipartFormDataContent();
multiForm.Add(new StringContent("1/12/1.jpg"), "custom_file_name");
FileStream fs = System.IO.File.OpenRead("1.jpg");
multiForm.Add(new StreamContent(fs), "file", "1.jpg");
// send request to API
var url = "https://localhost:123/api/store";
var response = await client.PostAsync(url, multiForm);
But the server returns HTTP Code 404 Not Found.
How can I correctly tweak the Store method above so it accepts the file being sent by the client?
You shouldn't include api/ in the action method route. The controller's route is already prefixed with api. Also, api/[controller] means the controller route will be api/ControllerName, then your action will be api/ControllerName/store.
If your controller class is named ThingsController then its route is api/things, and your Store action should be api/things/store.
You don't need to use both HttpPost and Route attributes on an action. The Route attribute accepts all HTTP verbs, so use one or the other (typically just HttpPost or whatever HTTP verb you will be handling with the action method. So:
[ApiController]
[Route("api/[controller]")]
public class ThingsController : ControllerBase
{
// POST api/things/store
[HttpPost("store")]
public async Task<IActionResult> Store([FromBody] MultipartFormDataContent content)
{
// Do stuff
}
}
Also, the controller should be using IFormFile to deal with file uploads in your controller. The documentation is here:
Upload files in ASP.NET Core .
Your controller would accept a IFormFile files parameter rather than MultipartFormDataContent content.
Example from MSDN, adapted to your action
[HttpPost("store")]
public async Task<IActionResult> Store(List<IFormFile> files)
{
// Get total size
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
// Gets a temporary file name
var filePath = Path.GetTempFileName();
// Creates the file at filePath and copies the contents
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size, filePath });
}
If you're dealing with a single file, you can just pass IFormFile file instead of a list, and get rid of the looping in the action method.
I have not tested this code myself, wrote it on a mobile device. Hopefully it solves the problem though.
Related
I'm experiencing a problem when trying to use MultipartFormDataContent with HttpClient with a stream of data.
Context
I'm trying to upload a large file to ASP.NET Core Web API. A client should send the file via POST request form-data to a front-end API, which in turn should forward the file to a back-end API.
Because the file can be large, I followed the Microsoft example, i.e. I don't want to use IFormFile type but instead read the Request.Body using MultipartReader. This is to avoid loading the entire file into memory on the server, or saving it in a temporary file on server's hard drive.
Problem
The back-end API controller action looks as follows (this is almost directly copied from the ASP.NET Core 5.0 sample app with just minor simplifications):
[HttpPost]
[DisableRequestSizeLimit]
public async Task<IActionResult> ReceiveLargeFile()
{
var request = HttpContext.Request;
if (!request.HasFormContentType
|| !MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader)
|| string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value))
{
return new UnsupportedMediaTypeResult();
}
var reader = new MultipartReader(mediaTypeHeader.Boundary.Value, request.Body);
/* This throws an IOException: Unexpected end of Stream, the content may have already been read by another component. */
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition,
out var contentDisposition);
if (hasContentDispositionHeader
&& contentDisposition!.DispositionType.Equals("form-data")
&& !string.IsNullOrEmpty(contentDisposition.FileName.Value))
{
/* Fake copy to nothing since it doesn't even get here */
await section.Body.CopyToAsync(Stream.Null);
return Ok();
}
section = await reader.ReadNextSectionAsync();
}
return BadRequest("No files data in the request.");
}
I managed to reduce the problem slightly by making an integration test using Microsoft.AspNetCore.Mvc.Testing NuGet package. The following test replaces the front-end API, so instead of reading Request.Body stream in a Web API, the test just tries to add StreamContent to MultipartFormDataContent and post it via HttpClient to the back-end API:
[Fact]
public async Task Client_posting_to_Api_returns_Ok()
{
/* Arrange */
await using var stream = new MemoryStream();
await using var writer = new StreamWriter(stream);
await writer.WriteLineAsync("FILE CONTENTS");
await writer.FlushAsync();
stream.Position = 0;
using var client = _factory.CreateDefaultClient();
/* Act */
using var response =
await client.PostAsync(
"Receive",
new MultipartFormDataContent
{
{
new StreamContent(stream),
"file",
"fileName"
}
});
/* Assert */
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
The back-end API controller then throws an IOException at await reader.ReadNextSectionAsync(), saying "Unexpected end of Stream, the content may have already been read by another component".
GitHub Repository (Complete Example)
I uploaded a complete example of the problem (including back-end API and the test) a GitHub repo.
Question
I must be doing something wrong. How can I forward a file received in a request with form-data content type in one service (front-end API) to another service (back-end API) without loading the entire file into memory or hard-drive in the front-end API, i.e. to just forward the stream of data to the back-end API?
Thanks in advance for any help.
I expected the same issue as you and it turned out that the MediaTypeHeaderValue.TryParse method parses the boundary value wrong as it wraps the string with '"' characters, because HttpClient sends the content type header like this:
multipart/form-data; boundary="blablabla"
So for me the solution was to add a Trim() method to boundary like this and pass that to the MultipartReader
var boundary = mediaTypeHeader.Boundary.Value.Trim('"');
var reader = new MultipartReader(boundary, request.Body);
I got a Web-Api with the following simplified method for obtaining a file:
[HttpPut("Test/{id}")]
public IActionResult PutTest(string id)
{
Stream file = Request.Body;
//rest of method
return StatusCode(201);
}
It works fine, although in the SwaggerUI page that gets created there's no mention of the method expecting a file in the body. Is there a way to specify the method expects a file in the generated SwaggerUI page?
I can simulate the input using the following piece of code:
var content = new StreamContent(new FileStream("C:\temp\test.txt", FileMode.Open));
using (var client = new HttpClient())
using (var response = client.PutAsync(url, content))
{
var result = response.Result;
//more code
}
The file upload is not documented because it's not a param in the action method. Swagger has no way of knowing what you're doing inside the action itself. Attempting to handle a file upload this way is bad practice anyways. You can use IFormFile, but that only works if your request body is encoded as multipart/form-data. If you're dealing with JSON or basically anything that qualifies as FromBody, then you need to bind to a byte[]:
[HttpPut("Test/{id}")]
public IActionResult PutTest(string id, byte[] file)
{
//rest of method
return StatusCode(201);
}
Now, the automatic FromBody applied by the [ApiController] attribute only works with class types, so I'm not sure off the top of my head whether it will apply to a byte[]. If not, just do:
public IActionResult PutTest(string id, [FromBody]byte[] file)
I have found a bunch of examples that use objects not available to me within my application and don't seem to match up to my version of .NET Core web API. In essence I am working on a project that will have <video> tags on a web page and want to load the videos using a stream from the server rather than directly serving the files via a path. One reason is the source of the files may change and serving them via path isn't what my customer wants. So I need to be able to open a stream and async write the video file.
This for some reason produces JSON data so that's wrong. But I just don't understand what I need to do to send a streamed video file to a <video> tag in HTML.
Current Code:
[HttpGet]
public HttpResponseMessage GetVideoContent()
{
if (Program.TryOpenFile("BigBuckBunny.mp4", FileMode.Open, out FileStream fs))
{
using (var file = fs)
{
var range = Request.Headers.GetCommaSeparatedValues("Range").FirstOrDefault();
if (range != null)
{
var msg = new HttpResponseMessage(HttpStatusCode.PartialContent);
var body = GetRange(file, range);
msg.Content = new StreamContent(body);
msg.Content.Headers.Add("Content-Type", "video/mp4");
//msg.Content.Headers.Add("Content-Range", $"0-0/{fs.Length}");
return msg;
}
else
{
var msg = new HttpResponseMessage(HttpStatusCode.OK);
msg.Content = new StreamContent(file);
msg.Content.Headers.Add("Content-Type", "video/mp4");
return msg;
}
}
}
else
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
}
HttpResponseMessage is not used as a return type in asp.net-core it will read that as an object model and serialize it in the response by design, as you have already observed.
Luckily in ASP.NET Core 2.0, you have
Enhanced HTTP header support
If an application visitor requests content with a Range Request header, ASP.NET will recognize that and handle that header. If the requested content can be partially delivered, ASP.NET will appropriately skip and return just the requested set of bytes. You don't need to write any special handlers into your methods to adapt or handle this feature; it's automatically handled for you.
So now all you have to do is return the file stream
[HttpGet]
public IActionResult GetVideoContent() {
if (Program.TryOpenFile("BigBuckBunny.mp4", FileMode.Open, out FileStream fs)) {
FileStreamResult result = File(
fileStream: fs,
contentType: new MediaTypeHeaderValue("video/mp4").MediaType,
enableRangeProcessing: true //<-- enable range requests processing
);
return result;
}
return BadRequest();
}
Making sure to enable range requests processing. Though, as stated in the documentation, that should be handled based on the request headers and whether that data can be partially delivered.
From there it is now a simple matter of pointing to the endpoint from the video client and let it do its magic
i am returning an audio file from web api. requirement is to play the media file instead of downloading,i am using this code.
[HttpGet]
[Route("audiofile/download", Name = "GetAudioFile")]
public HttpResponseMessage GetAudioFile(string q)
{
if (string.IsNullOrWhiteSpace(q))
{
return new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest };
}
String path = HttpContext.Current.Server.MapPath("~/AudioUploads/");
string filePath = Path.Combine(path, q);
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StreamContent(File.OpenRead(filePath))
};
var contentType = MimeMapping.GetMimeMapping(Path.GetExtension(filePath));
response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
return response;
}
i noticed that this action method is being hit twice as like
Can anyone suggest why its happening? why my api method is being called twice?
P.S I am using Url.Link in order to make uploaded file url. when i hit that, api method is called twice.
Servers only respond to requests. They can't initiate a communication with a client, without initial request.
That said, your client code is to blame here, as it's sending two requests instead of one, and the server correctly responds to both.
As you can see, I am trying to send an image, and a name through a POST command to a local function.
How Can I read both of these parameters in C#?
This is what I have tried but It can only read File Image.
[FunctionName("Test")]
public static async Task<HttpResponseMessage>
Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route =
null)]HttpRequestMessage req, TraceWriter log)
{
//Check if the request contains multipart/form-data.
if (!req.Content.IsMimeMultipartContent())
{
return req.CreateResponse(HttpStatusCode.UnsupportedMediaType);
}
foreach (var stream in contents.Contents)
{
try
{
var fileBytes = await stream.ReadAsByteArrayAsync();
var fileinfo = new FileInfo(stream.Headers.ContentDisposition.FileName.Trim('"'));
//Can Read File image like this.
}
catch(Exception e)
{
return req.CreateErrorResponse(HttpStatusCode.Conflict, e);
}
Is there a workaround to do this like using memory stream?
According to your requirement, I assumed that you could use HttpContentMultipartExtensions.ReadAsMultipartAsync and you would get the MultipartMemoryStreamProvider, then you could leverage the following code for reading your uploaded files:
var multipartMemoryStreamProvider= await req.Content.ReadAsMultipartAsync();
foreach (HttpContent content in multipartMemoryStreamProvider.Contents)
{
// for reading the uploaded file
var filename= content.Headers.ContentDisposition.FileName.Trim('"');
var stream=await content.ReadAsStreamAsync();
//for formdata, you could check whether `content.Headers.ContentDisposition.FileName` is empty
log.Info($"name={content.Headers.ContentDisposition.Name},value={await content.ReadAsStringAsync()}");
}
Moreover, you could follow this issue about creating your custom MultipartFormDataMemoryStreamProvider based on MultipartMemoryStreamProvider. And this issue about building custom InMemoryMultipartFormDataStreamProvider based on MultipartStreamProvider.
You can use MultipartFormDataStreamProvider
Use FileData property to get the posted files
Use FormData property to get the values of any form data posted based upon the key.
Check this File Upload and Multipart MIME.