I see lots of examples of WebAPIs accepting files. However, I have yet to find a solution as to why, no matter what I've tried, to get my HttpContext.Current.Request.Files to ever have a file in it that I am posting to the Web API through Postman. (I have also posted that image through a Console Application and got the same result)
[HttpPost]
[ActionName("Post")]
[ResponseType(typeof(PeliquinApiRsp))]
public IHttpActionResult Post(int personId)
{
var empPicture = PeliquinIOC.Resolve<IEmpPictureBL>(UserId, UserName, PropertyCode);
if (!(IsAllowed(SysPrivConstants.SYSPRIV__TYPE_PERSONNEL, SysPrivConstants.SYSPRIV__FUNC_PERSONNEL_CARDHOLDER, SysPrivConstants.SYSPRIV__LEVEL_FULL)))
return (Unauthorized());
var apiRsp = new PeliquinApiRsp();
var httpRequest = HttpContext.Current.Request;
if (httpRequest.Files.Count == 0)
return BadRequest();
empPicture.Post(httpRequest.Files[0].InputStream, personId);
apiRsp.SetStatus();
apiRsp.SetData("EmpPicture");
return (Ok(apiRsp));
}
It is a very simple method. I've been using Postman to post a Binary .jpg file. I have set the content-type = "multipart/form-data". No error is throw in my code, other than the .Files = 0.
I have some additional settings in my WebApiConfig for the rest of the API, but I'll include them just in case, along with the route for this particular method:
config.Formatters.Remove( config.Formatters.XmlFormatter );
config.Formatters.JsonFormatter.SupportedMediaTypes.Add( new MediaTypeHeaderValue( "application/json" ) );
config.Formatters.FormUrlEncodedFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
config.Routes.MapHttpRoute(
name: "EmpPicturePost",
routeTemplate: "api/EmpPicture/{personId}",
defaults: new { controller = "EmpPicture", action = "Post" },
constraints: new { personId = #"^\d+$", httpMethod = new HttpMethodConstraint(HttpMethod.Post, HttpMethod.Options) }
);
I am at wit's end trying to figure out why something so simple, just doesn't work. I've tried quite a few other way of doing this, that were all more MVC-like, but this isn't an MVC app, and they threw different errors. Thanks for any help.
I also struggle with this issue for a while but eventually found a solution. In my case i was calling the endpoint from an iOS app and the files where always empty. I found that it I need to have a http header that matches the file field name in the form so the request would have to have an http header for example:
file: newImage.jpg
and inside the body of the request you would need something like
Content-Disposition: form-data; name="file"; filename="newImage.jpg"
Content-Type: image/jpg
Please be aware that this is not a valid request just a snippet. You still need more http headers and the boundaries around the fields in the body. My point is that it seem the requirement is that http header key and the form field name match and both exist.
I suspect in all likely hood the root cause of your problem is because of this what you've mentioned in your post -
I've been using Postman to post a Binary .jpg file. I have set the
content-type = "multipart/form-data"
You should not be doing so explicitly in postman tool. Postman tool is smart enough to auto-detect Content-Type on its own. Please see this answer of mine to help you more with it.
Setting up Content-Type to "multipart/form-data" involves a complex concept of boundaries of multiple parts as detailed here. Heavy lifting of setting up the boundaries is done automatically for you by postman tool which is why it doesn't want you to set the content-type explicitly in this case.
I think Postman is not capable of filling the HttpContext.Current.Request.Files
collection. We fixed the problem by using:
await Request.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
Ivo
Related
So quick background on what is currently going on in my solution. I'm writing pretty basic API web tests in c# using the XUnit framework and some json validation helpers I created to validate response messages and payloads received back from the API. I've been banging my head against a wall trying to figure out why my PostAsync() call is passing null once it reaches the API since I can see the request body is a valid json request when debugging my ApiTests solution. Below are some snippets of the test I'm currently debugging.
OrderControllerTests.cs
[Theory]
[MemberData(nameof(OrderControllerData.postCancelOrder), MemberType = typeof(OrderControllerData))]
public async Task PostValidCancelOrder(string jsonBody, string expectedResponseBody)
{
var postObject = "api/Order/cancel";
var request = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(postObject, request);
var content = await response.Content.ReadAsStringAsync();
ValidationHelper.ValidateNoErrors(response);
ValidationHelper.ValidateJson(content, expectedResponseBody);
}
A few things are happening in this test. All of which seem to be working fine and as intended (except for maybe #3 below).
Create my requestUri with postObject.
Create my HttpContent. XUnit is passing my jsonBody param by way of the MemberData attribute. Which looks into my data layer class for the postCancelOrder public static IEnumerable.
Posting the request to the API.
Reading the payload returned back in string format.
Validation Helper checks for HTTP response messages. The ValidateNoErrors method is simply just looking for a 200 response code. The second one doesn't matter for this question since my test blows up at the first ValidationHelper.
Right now this is currently what I am posting to the api.
"{\"orderId\": \"c701b60a-4fb1-4f79-9e17-0172a9a17bbc\",\"test1Fee\":\"5}\",\"test2Fee\":\"10\"}"
Here is the HttpPost within the Api/OrderController.
OrderController.cs
[HttpPost("cancel")]
public async Task<IActionResult> Cancel([FromBody]CancelOrderViewModel model)
{
var command = new CancelOrderCommand(model.OrderId, model.Test1Fee, model.Test2Fee);
var order = await _mediator.Send(command);
return Ok(order);
}
Essentially this HttpPost is passing a few parameters and then running a multiple methods and checks on those objects to determine if the order is eligible to be cancelled. What happens within the post doesn't matter right now. When debugging my test and it hits the entry point of the HttpPost method of the api, the 'command' and 'model' are both null. Naturally the Api will reject the request and responds with a 500 error.
{StatusCode: 500, ReasonPhrase: 'Internal Server Error', Version: 1.1, Content: System.Net.Http.NoWriteNoSeekStreamContent, Headers:{ Date: Mon, 22 Jan 2018 19:38:55 GMT Transfer-Encoding: chunked Server: Kestrel Request-Context: appId=cid-v1:93254013-d606-465f-8cee-12e422316f31 X-SourceFiles: =?UTF-8?B?RDpcUUFccWFWZW5kb3JcQWJzdHJhY3Rvck9tc0FwaVxzb3VyY2VcQXBpXGFwaVxPcmRlclxjYW5jZWw=?= X-Powered-By: ASP.NET Content-Type: application/json; charset=utf-8}}
Also the message is indicating that Object reference not set to an instance of an object since everything is null apparently.
So the problem has to be with how I'm constructing the request within my XUnit test. But I'm confused because I believe I'm serializing the json correctly. Even when the Api is running locally in my browser using swagger, the indicated example is
{
"orderId": "string",
"test1Fee": 0,
"test2Fee": 0
}
Could the escapes being created within my post object be the problem? If so, how do I go about fixing that? I tried using PostAsJsonAsync() instead of just PostAsync() but it continues to yield the same results.
What am I doing wrong here? I'm sure it's something pretty simple and I'm just missing it. Let me know if I need to provide anymore information. Any help would be greatly appreciated. Thanks.
I am using postman to test my .net core API, when i am trying to post data via postman form-data this returns a 415 even if i set the Content-Type header to application/json as the common solution for this issue appears to be online.
If i fire the request without any files via the raw postman option and set the content type as JSON(application/json) this request reaches the API successfully.
Here is how my API looks:
[HttpPost("{organization}")]
public IActionResult Post([FromBody] Asset asset, string organization)
{
//Api body
//Get files from request
Task uploadBlob = BlobFunctions.UploadBlobAsync(_blobContainer,Request.Form.Files[0]);
}
And here is how the failed postman request looks like
and the header for that request
What else am i missing for this to work?
Small update
This works fine if i remove [FromBody]Asset asset and just pass the file
Try using the [FromForm] attribute instead of the [FromBody] attribute:
[HttpPost("{organization}")]
public IActionResult Post([FromForm] string asset, string organization, IFormFile fileToPost)
{
//Api body
Asset asset = JsonConvert.DeserializeObject<Asset>(asset);
//Get files from request
Task uploadBlob = BlobFunctions.UploadBlobAsync(_blobContainer, fileToPost);
}
I can't say for sure, but my guess is that in postman, since you're making a form-data request, your content-type would end up being "multipart/form-data" (if you debug the request when it is being processed, you can see that the content type changes to multipart even though you set it to application/json).
But in your Controller's POST action you specify that you expect an Asset object from the body (which expects a JSON object by default). So you get a 415 since your request's content type is multipart while your API expects application/json because you used the [FromBody] attribute.
Turns out that for some weird reason I was not allowed to pass any of them as a variable of my controller but it works if I retrieve both from the Request.
if (!Request.Form.ContainsKey("asset"))
{
return BadRequest("Asset cannot be empty");
}
Asset asset = JsonConvert.DeserializeObject<Asset>(Request.Form.First(a => a.Key == "asset").Value);
and for the file
var file = equest.Form.Files[0]
Not sure why this is the case and would appreciate if someone could explain this to me but this seems to solve my issue.
I have a web application using MVC and AngularJS, which connects to a Web API 2 api, that I have set up in a separate project.
Currently I am able to retrieve information from the Api with no problems.
However when I try to do a HTTP Post I am getting no response, originally I was getting a problem with the pre-flight request failing, I have now handled this in my controller, however it does not send the proper request after it has got an OK message back.
I have included my code for the Angular Factory and the C# Controller in the API.
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class RegisterController : ApiController
{
public string Post()
{
return "success";
}
public HttpResponseMessage Options()
{
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
}
}
var RegistrationFactory = function($http, $q, ApiAddress) {
return function(model) {
// $http.post(ApiAddress.getApiAddress() + '/Register/Post', model.ToString());
$http({
method: "POST",
url: ApiAddress.getApiAddress() + '/Register/Post',
data: model,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}).success(function(data) {
$location.path("/");
});
}
};
RegistrationFactory.$inject = ['$http', '$q', 'ApiAddress'];
Edit:
I am still not having any joy with this, however I tested in Internet Explorer and it works with no problems at all.
I have got it working in chrome by starting with web security disabled, however obviously this is not ideal as it will not work on a user PC with security enabled.
I see that you have done adaptation for CORS on the server side. But I cannot see any client side (javascript) adaptation. May be you should add the code below before calling the service.
$http.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];
Let me know if this fixes the issue. Worked for me in all scenarios :)
It's strange that your GETs work, but your POSTs don't.
I would recommend running the code in Google Chrome with web security enabled (so we can watch it go wrong) and with the F12 Developer Options shown.
Select the Network tab, run your code, and watch what happens when the POST is called.
Does your service return a "200 OK" status, or some other value ?
Does any kind of Response get returned ?
It might be worth trying this, and appending a screenshot of the results in your original question. It might help to identify the cause.
I am still not having any joy with this, however I tested in Internet
Explorer and it works with no problems at all.
Btw, you don't have any single sign-on stuff setup in your company, do you ? We've had issues where IE works fine, but other browsers don't allow single sign-on. Just a thought...
CORS requires a OPTIONS-preflight which has HTTP headers in its response that tell the browser whether it is allowed to access the resource.
E.g. HTTP Response Headers:
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Origin: *
Because you have a custom Options handler in your C# controller, it seems those HTTP headers are not returned, stopping the browser to make the call after the preflight.
Avoid the Options method, and you should be good.
I'm using the Amazon .NET SDK to generate a pre-signed URL like this:
public System.Web.Mvc.ActionResult AsActionResult(string contentType, string contentDisposition)
{
ResponseHeaderOverrides headerOverrides = new ResponseHeaderOverrides();
headerOverrides.ContentType = contentType;
if (!string.IsNullOrWhiteSpace(contentDisposition))
{
headerOverrides.ContentDisposition = contentDisposition;
}
GetPreSignedUrlRequest request = new GetPreSignedUrlRequest()
.WithBucketName(bucketName)
.WithKey(objectKey)
.WithProtocol(Protocol.HTTPS)
.WithExpires(DateTime.Now.AddMinutes(6))
.WithResponseHeaderOverrides(headerOverrides);
string url = S3Client.GetPreSignedURL(request);
return new RedirectResult(url, permanent: false);
}
This works perfectly, except if my contentType contains a + in it. This happens when I try to get an SVG file, for example, which gets a content type of image/svg+xml. In this case, S3 throws a SignatureDoesNotMatch error.
The error message shows the StringToSign like this:
GET 1234567890 /blah/blabh/blah.svg?response-content-disposition=filename="blah.svg"&response-content-type=image/svg xml
Notice there's a space in the response-content-type, where it now says image/svg xml instead of image/svg+xml. It seems to me like that's what is causing the problem, but what's the right way to fix it?
Should I be encoding my content type? Enclose it within quotes or something? The documentation doesn't say anything about this.
Update
This bug has been fixed as of Version 1.4.1.0 of the SDK.
Workaround
This is a confirmed bug in the AWS SDK, so until they issue a fix I'm going with this hack to make things work:
Specify the content type exactly how you want it to look like in the response header. So, if you want S3 to return a content type of image/svg+xml, set it exactly like this:
ResponseHeaderOverrides headerOverrides = new ResponseHeaderOverrides();
headerOverrides.ContentType = "image/svg+xml";
Now, go ahead and generate the pre signed request as usual:
GetPreSignedUrlRequest request = new GetPreSignedUrlRequest()
.WithBucketName(bucketName)
.WithKey(objectKey)
.WithProtocol(Protocol.HTTPS)
.WithExpires(DateTime.Now.AddMinutes(6))
.WithResponseHeaderOverrides(headerOverrides);
string url = S3Client.GetPreSignedURL(request);
Finally, "fix" the resulting URL with the properly URL encoded value for your content type:
url = url.Replace(contentType, HttpUtility.UrlEncode(contentType));
Yes, it's a dirty workaround but, hey, it works for me! :)
Strange indeed - I've been able reproduce this easily, with the following observed behavior:
replacing + in the the URL generated by GetPreSignedURL() with its encoded form %2B yields a working URL/signature
this holds true, no matter whether / is replaced with its encoded form %2F or not
encoding the contentType upfront before calling GetPreSignedURL(), e.g. via the HttpUtility.UrlEncode Method, yields invalid signatures regardless of any variation of the generated URL
Given how long this functionality is available already, this is somewhat surprising, but I'd still consider it to be a bug - accordingly it might be best to inquiry about this in the Amazon Simple Storage Service forum.
Update: I just realized you asked the same question there already and the bug got confirmed indeed, so the correct answer can be figured out over time by monitoring the AWS team response ;)
Update: This bug has been fixed as of Version 1.4.1.0 of the SDK.
I have a simple form that uploads an image to a database. Using a controller action, the image can then be served back (I've hard coded to use jpegs for this code):
public class ImagesController : Controller
{
[HttpPost]
public ActionResult Create(HttpPostedFileBase image)
{
var message = new MessageItem();
message.ImageData = new byte[image.ContentLength];
image.InputStream.Read(message.ImageData, 0, image.ContentLength);
this.session.Save(message);
return this.RedirectToAction("index");
}
[HttpGet]
public FileResult View(int id)
{
var message = this.session.Get<MessageItem>(id);
return this.File(message.ImageData, "image/jpeg");
}
}
This works great and directly browsing to the image (e.g. /images/view/1) displays the image correctly. However, I noticed that when FireBug is turned on, I'm greeted with a lovely error:
Image corrupt or truncated: data:image/jpeg;base64,/f39... (followed by the base64 representation of the image).
Additionally in Chrome developer tools:
Resource interpreted as Document but transferred with MIME type image/jpeg.
I checked the headers that are being returned. The following is an example of the headers sent back to the browser. Nothing looks out of the ordinary (perhaps the Cache-Control?):
Cache-Control private, s-maxage=0
Content-Type image/jpeg
Server Microsoft-IIS/7.5
X-AspNetMvc-Version 3.0
X-AspNet-Version 4.0.30319
X-SourceFiles =?UTF-8?B?(Trimmed...)
X-Powered-By ASP.NET
Date Wed, 25 May 2011 23:48:22 GMT
Content-Length 21362
Additionally, I thought I'd mention that I'm running this on IIS Express (even tested on Cassini with the same results).
The odd part is that the image displays correctly but the consoles are telling me otherwise. Ideally I'd like to not ignore these errors. Finally, to further add to the confusion, when referenced as an image (e.g. <img src="/images/view/1" />), no error occurs.
EDIT: It is possible to fully reproduce this without any of the above actions:
public class ImageController : Controller
{
public FileResult Test()
{
// I know this is directly reading from a file, but the whole purpose is
// to return a *buffer* of a file and not the *path* to the file.
// This will throw the error in FireBug.
var buffer = System.IO.File.ReadAllBytes("PATH_TO_JPEG");
return this.File(buffer, "image/jpeg");
}
}
You're assuming the MIME type is always image/jpeg, and your're not using the MIME type of the uploaded image. I've seen this MIME types posted by different browsers for uploaded images:
image/gif
image/jpeg
image/pjpeg
image/png
image/x-png
image/bmp
image/tiff
Maybe image/jpeg is not the correct MIME type for the file and the dev tools are giving you a warning.
Could it be that the session.Save/Get is truncating the jpeg?
Use Fiddler and save this file on the server. Attempt a GET request directly to the image. Then attempt the GET to the action method. Compare fiddler's headers and content (can save out and compare with a trial of BeyondCompare). If they match for both get requests - well.. that wouldn't make sense - something would be different in that case and hopefully point to the issue. Something has to be different - but without seeing the fiddler output its hard to say : )
Could it possibly be that the image itself is corrupt? If you save it as a file on your website and access it directly does the error come up? How does that request look compared to your action request in Fiddler? It could be the browsers are trying to get the content type by extension, you could try a route like this to see if there are any changes:
routes.MapRoute(
"JpegImages",
"Images/View/{id}.jpg",
new { controller = "Images", action = "View" }
);
One more thing to check. image.InputStream.Read() returns an integer which is the actual number of bytes read. It may be that all the bytes aren't able to be read at once, can you record that and throw an error if the numbers don't match?
int bytesRead = image.InputStream.Read(message.ImageData, 0, image.ContentLength);
if (bytesRead != image.ContentLength)
throw new Exception("Invalid length");
I wonder if it is something to do with X-SourceFiles. I'm doing the exact same thing as you with MVC but I am persisting my byte array in the database. The only difference I don't understand in our headers is the X-SourceFiles.
Here is something about what X-SourceFiles does What does the X-SourceFiles header do? and it talks about encoding. So maybe?? The answerer claims this only happens on your local host by the way.
As far as I understand it if you are returning a proper byte array that is a jpeg then your code should work fine. That is exactly what I'm doing successfully (without an X-SourceFiles header).
Thanks everyone for all the help. I know this is is going to be a very anti-climatic ending for this problem, but I was able to "resolve" the issue. I tried building my code from another machine using the same browser/firebug versions. Oddly enough, no errors appeared. When I went back to the other machine (cleared all cache and even re-installed browser/firebug) it was still getting the error. What's even more weird is that both Chrome/Firefox are now showing the error when I visit other websites.
Again, thanks everyone for all their suggestions!