I'm working with an HttpSelfHostServer in .Net 4.5 and it seems to only be able to determine the controller and action when I send a request using QueryString. It doesn't work if I use "application/x-www-form-urlencoded".
Here's the HttpSelfHostServer code.
private static HttpSelfHostConfiguration _config;
private static HttpSelfHostServer _server;
public static readonly string SelfHostUrl = "http://localhost:8989";
internal static void Start()
{
_config = new HttpSelfHostConfiguration(SelfHostUrl);
_config.HostNameComparisonMode = HostNameComparisonMode.Exact;
_config.Routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{action}",
defaults: new { action = RouteParameter.Optional },
constraints: null);
_server = new HttpSelfHostServer(_config);
_server.OpenAsync().Wait();
}
The controller code.
public class SettingsController : ApiController
{
[HttpPost]
public bool Test(bool work)
{
return work;
}
}
Here is the response I get when attempting to access it via REST Console using
Request URL: http://localhost:8989/api/Settings/Test
Request Method:POST
Status Code:404 Not Found
Request Headersview source
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Content-Length:9
Content-Type:application/x-www-form-urlencoded
Host:localhost:8989
Origin:chrome-extension://cokgbflfommojglbmbpenpphppikmonn
Pragma:no-cache
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Form Dataview parsed
work=true
Response Headersview source
Content-Length:205
Content-Type:application/json; charset=utf-8
Date:Mon, 23 Dec 2013 22:41:10 GMT
Server:Microsoft-HTTPAPI/2.0
So, if I change my request to a post to the url below, it works.
http://localhost:8989/api/Settings/Test?work=true
Why isn't Content-Type:application/x-www-form-urlencoded working?
Your action method parameter is bool, which is a simple type. By default, ASP.NET Web API populates it from URI path or query string. That is the reason http://localhost:8989/api/Settings/Test?work=true works.
When you send the same in the request body, it does not work because ASP.NET Web API binds body to a complex type (class) by default and hence the body will not be bound to your argument type bool. To ask Web API to bind the simple type from body, change your action method like this.
public bool Test([FromBody]bool work)
Then, you will need to send only the value in the body, like this.
=true.
The problem is not with the Content-Type. The rest console uses AJAX and CORS, see http://en.wikipedia.org/wiki/Cross-origin_resource_sharing. This is indicated by the Origin http header in the request.
The server that contains the SettingsController must support CORS. If it doesn't the AJAX request will always return a 404.
The easiest way for you to support CORS is to always return a Access-Control-Allow-Origin: * in the request HTTP headers.
Related
I want to create a back-end by ASP.NET Web Application API for a SAPUI5 front-end.
The front-end side is so simple and works with mock-server fine, and has no problem with internal mock-server.
However I want to use the app with a real ASP.NET back-end server.
I used this tutorial for creating the ASP.NET back-end server.
The only part that is a little bit different from the original tutorial is the WebAppConfig.cs file which I revealed it here:
using CimplySkills_Backend.Models;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.Data.Edm;
using Microsoft.Data.Edm.Csdl;
using Microsoft.OData.Edm.Csdl;
using System.Web.Http.OData.Batch;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Extensions;
using System;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Cors;
namespace CimplySkills_Backend
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// allow CORS
var cors = new EnableCorsAttribute("*", "*", "*", "*");
config.EnableCors(cors);
// Web-API-Routen
config.MapHttpAttributeRoutes();
// define HTTP access
/*
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
*/
// enable batch
var odataBatchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
// enable queries
config.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// set entities
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<CountriesSet>("CountriesSet");
builder.Entity<LocationsSet>().HasOptional(x => x.Country).CascadeOnDelete(false);
//Define version
Version odataVersion2 = new Version(2, 0);
builder.DataServiceVersion = odataVersion2;
builder.MaxDataServiceVersion = odataVersion2;
IEdmModel edmModel = builder.GetEdmModel();
edmModel.SetEdmVersion(odataVersion2);
edmModel.SetEdmxVersion(odataVersion2);
config.Routes.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "sap/opu/odata/cimtag/ODATA_SKILLS_SRV/",
// routePrefix: null,
model: edmModel,
batchHandler: odataBatchHandler);
// resolve "No non-OData HTTP route registered"
// config.EnableDependencyInjection();
}
}
}
Now the problem is when the UI5 app try to send a batch request to the back-end, it can send the request and access the end-point correctly, the response is also successfully received, however there is a problem in the response that it will repeat the request again and again, and the binding is not triggered. Look at the following picture:
First of all, it seems I am not the only person who had this problem, for example here somebody else also had the same problem.
I analyzed the response from this ASP.NET server with a same batch response to an SAP ABAP server (which is the standard back-end for SAPUI5 applications) and there are a few differences.
For example here a batch response in SAP:
--BD335BC5DB87B4F290C21DB6C0A9D3650
Content-Type: application/http
Content-Length: 538
content-transfer-encoding: binary
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 347
dataserviceversion: 2.0
sap-metadata-last-modified: Tue, 07 Apr 2020 13:02:03 GMT
cache-control: no-store, no-cache
{
"d":{
"__metadata":{
"id":"https://xyz.de:5698/sap/opu/odata/sap/ZMJZ_ODATA_COMMON_SRV/AuthSet'ZMJZ_SECTY')", "uri":"https://xyz.de:5698/sap/opu/odata/sap/ZMJZ_ODATA_COMMON_SRV/AuthSet'ZMJZ_SECTY')", "type":"ZMJZ_ODATA_COMMON_SRV.Auth"
},
"Object":"ZMJZ_SECTY",
"FlagCreate":true,
"FlagRead":true,
"FlagUpdate":true,
"FlagDelete":true
}
}
--BD335BC5DB87B4F290C21DB6C0A9D3650--
And here a batch response from ASP.NET:
--batchresponse_988aa133-3a4a-413c-9109-f5f7786e7e78
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 2.0
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: *
{
"d":{
"results":[
{
"__metadata":{
},"ID":1,"name":"Germany"
},{
"__metadata":{
},"ID":2,"name":"France"
},{
"__metadata":{
},"ID":3,"name":"Iran"
},{
"__metadata":{
},"ID":4,"name":"USA"
}
]
}
}
--batchresponse_988aa133-3a4a-413c-9109-f5f7786e7e78--
If we look carefully, we will see there is a Content-Length property that is appear two times in the SAP response while it does not exist in the ASP.NET response at all. (It is only my guess that it is the source of the problem)
The first question of mien is how can I modify the batch response in ASP.NET to add the Content-Length in the response content also. (Please notice that the Content-Length exist inside the response header and what you see here is the response payload or the response content itself!)
The second question is more general, do you have any other suggestion for fixing this issue?
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"
}
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.
Edit 1: Other Controller
public class identityController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> getfullname(string firstName)
{
string name = firstName;
return Ok(name);
}
}
I have created a controller which uses an API from another solution.
Method that i use in the controller looks like below:
public class GetNameController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> CalculatePrice(string firstName)
{
string _apiUrl = String.Format("api/identity/getfullname?firstName={0}", firstName);
string _baseAddress = "http://testApp.azurewebsites.net/";
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await client.GetAsync(_apiUrl);
if (response.IsSuccessStatusCode)
{
return Ok(response);
}
}
return NotFound();
}
The result of response.IsSuccessStatusCode is always false. When i check the response values i see this result:
{
StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Connection: close
Date: Thu, 21 Jul 2016 12:28:21 GMT
Server: Microsoft-HTTPAPI/2.0
Content-Length: 334
Content-Type: text/html; charset=us-ascii
}
}
What could i be missing?
string _apiUrl = String.Format("api/identity/{0}", firstName);
This is assuming that your url is correct, and your testapp is up and running. Even though when I hit it azure tells me your app is stopped. You will need to get your app started first, then change the string _apiUrl to the suggestion above.
http://testapp.azurewebsites.net/api/identity/getfullname?firstName=steve
Gives me this message
Error 403 - This web app is stopped.
The web app you have attempted to reach is currently stopped and does
not accept any requests. Please try to reload the page or visit it
again soon.
If you are the web app administrator, please find the common 403 error
scenarios and resolution here. For further troubleshooting tools and
recommendations, please visit Azure Portal.
So there are several things in your identity controller that are going on.
the functions name is getFullName. Since the word get is in the name of the function. Any httpget request will be routed to the function automagically. Thus making the [HttpGet] redundant. This only works if there is 1 and only 1 httpget request in your controller. If there are multiple you will need to fully qualify the url like you have done
Since youa re using the [httpget] method attribute I can assume you are using webapi2. That being the case and you are using a
primitive in your controller argument you can do notneed to fully
qualify the parameter name on your call. ?firstname={0} changes to
/{0}
I have an ASP.NET Web API. I am trying to POST a string to an endpoint. My ASP.NET Web API endpoint looks like the following:
[HttpPost]
public async Task<IHttpActionResult> Test(string name)
{
int i = 0;
i = i + 1;
return Ok();
}
In Fiddler, I execute the following request from the composer:
POST http://localhost:8089/api/MyApiController/test
If I remove "string name" as the parameter, I can successfully execute my API endpoint. When string name is there, I get a 405 error. So, I added the following in the "Request Body" section in Fiddler:
John
Unfortunately, that still causes a 405 to be thrown. I can't tell if I'm setting up my Web API endpoint wrong if I'm setting up my request in fiddler incorrectly. My full request looks like this:
POST http://localhost:8089/api/MyApiController/test HTTP/1.1
User-Agent: Fiddler
Host: localhost:8089
Content-Length: 26
Content-Type: application/json; charset=utf-8
{ "name" : "John"}
The response looks like this:
HTTP/1.1 405 Method Not Allowed
Cache-Control: no-cache
Pragma: no-cache
Allow: GET
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-SourceFiles: =?UTF-8?B?QzpcRWNvZmljXFNvbGlkUVxKTExcamxsLW1hcmtldHNwaGVyZVxXZWJzaXRlXGFwaVxTZWFyY2hBcGlcaW5kZXg=?=
X-Powered-By: ASP.NET
Date: Fri, 12 Dec 2014 15:51:50 GMT
Content-Length: 73
{"Message":"The requested resource does not support http method 'POST'."}
I don't understand why POST is allowed when I do not have a parameter. Yet, when I add a parameter, POST does not work.
[Update]
I added the following in my C# code:
var content = await Request.Content.ReadAsStringAsync();
I figured the JSON would be in the content. However, content is just an empty string.
Under the "Composer" tab:
I suggest you should try with following url
http://localhost:8089/api/MyApi/test
I think that problem is with route.
You should have some route like following as default route just have controller/id. This route comes before default route.
config.Routes.MapHttpRoute(
name: "DefaultApi1",
routeTemplate: "api/{controller}/{action}",
defaults: new { action="test" }
);
Also you action should look like this.
[HttpPost]
public async Task<IHttpActionResult> Test([Frombody]string name)
{
int i = 0;
i = i + 1;
return Ok();
}
in the body part of the composer try
{ "name" : "somtext"}
Below, I modified the datatype of the parameter from string to object, and had success.
public async Task<IHttpActionResult> Post( [ FromBody ] **object** items )
{
Console.WriteLine( items );
return Ok();
}
In the body you need to add Json or XML syntax, not just then name for The Asp.Net Deserialiser to do its thing. Json would be:
{ "name" : "John" }
EDIT: Your url route also seems incorrect, if you are using the default routes. By default it will be picked up as: POST http://localhost:8089/api/MyApiController/ the "test" part is omitted as it recognises this as a POST method. Your action should also specify that the parameter you take in ("name") is expected from the body public async Task<IHttpActionResult> Test([FromBody]string name). Now your url will match if you do something like:
`POST http://localhost:8089/api/MyApiController/`
User-Agent: Fiddler
Host: localhost:8089
Content-Length: 26
{ "name" : "John" }
I made a c# web api at the the moment . It is working fine until today.
I've tried to convert an image to a base64string, and then send the base64string to the c# server through ajax. When I did the said steps, an error occur.
XMLHttpRequest cannot load http://10.0.10.105:50231/api/hello. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://10.0.10.201' is therefore not allowed access.
I dont know where the main problem resides but in my observation, the error only occur when passing a very long base64string to the server because when i try to send short test string the problem wont appear and all is working well.
Do you have any idea what is the better remedy to fix this ? Or any other way to perform my needed objective ? my ajax code is something like this.
$.ajax({
type: 'POST', //GET or POST or PUT or DELETE verb
url: 'http://10.0.10.105:50231/api/hello', // Location of the service
contentType: 'application/x-www-form-urlencoded', // content type sent to server
data: { action: "3",pers_guid:"wew",base64image:"this-is-the-base64-image"},
//dataType: 'jsonp', //Expected data format from server
//processdata: true, //True or False
success: function (data) {//On Successfull service call
$scope.pageView = 2;
console.log("THE Data description for match : " + data[0].description);
},
error: function (msg) {// When Service call fails
alert(msg);
}
});
and my c# server is similar to this (this is not the whole code).
public class theHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken)
.ContinueWith((task) =>
{
HttpResponseMessage response = task.Result;
response.Headers.Add("Access-Control-Allow-Origin", "*");
return response;
});
}
}
public class HelloController : ApiController
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
//.... more code here
}
This is the result when I tried to pass a very long string...
Request URL:http://10.0.10.105:50231/api/hello
Request Headers CAUTION: Provisional headers are shown.
Accept:*/*
Content-Type:application/x-www-form-urlencoded
Origin:http://10.0.10.201
Referer:http://10.0.10.201/kiosk/
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36
Form Dataview sourceview URL encoded
action:3
pers_guid:wew
base64image:the_long_base64_string
but when i pass just a sample string this is the result.
Remote Address:10.0.10.105:50231
Request URL:http://10.0.10.105:50231/api/hello
Request Method:POST
Status Code:200 OK
Request Headersview source
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Content-Length:49
Content-Type:application/x-www-form-urlencoded
Host:10.0.10.105:50231
Origin:http://10.0.10.201
Referer:http://10.0.10.201/kiosk/
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36
Form Dataview sourceview URL encoded
action:3
pers_guid:wew
base64image:sample_string
Response Headersview source
Access-Control-Allow-Origin:*
Content-Length:103
Content-Type:application/json; charset=utf-8
Date:Wed, 04 Jun 2014 01:02:35 GMT
Server:Microsoft-HTTPAPI/2.0
This error shows up because your server listening on '10.0.10.105' does not specify the 'Access-Control-Allow-Origin' header in its HTTP response. This is a common issue with websites that 'talk' to each other, and you may read about it here (or just Google 'CORS').
As a solution, have the server listening on that IP return the following header from the POST response:
Access-Control-Allow-Origin: *
This has some security implications that you might want to read about (in general it's best to not use the "allow-all" star '*' and instead specify the requesting server explicitly).
Update: As explained in that paper under "Preflight requests", non-trivial POST requests require the server to also listen to OPTIONS requests and return these headers in the response:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Can you try having the image server listen to OPTIONS (just like you did for GET, POST, etc.) and return these headers? (The client making the 'POST' request will automatically precede it with an OPTIONS request to the same server, and if the OPTIONS response contains these headers, the subsequent POST will be called.)
Thank you so much for your answers. It gives me lot of ideas about access origin. It seems that my problem is on the configuration. I added this code and all is working fine.
config.MaxReceivedMessageSize = 5000000;
The program will automatically show an access-control-allow-origin when it exceeds the max limit size of the default configuration.
Thank you so much for the ideas.
So, CORS and the Access-Control-Allow-Origin header are awkward. I ran into this one a while back when I had an API that needed to be referenced from JavaScript code running on several different websites. I ended up writing an ActionFilterAttribute to handle it for my API controllers:
[AttributeUsage(AttributeTargets.Method)]
public class AllowReferrerAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var ctx = (System.Web.HttpContextWrapper)actionContext.Request.Properties["MS_HttpContext"];
var referrer = ctx.Request.UrlReferrer;
if (referrer != null)
{
string refhost = referrer.Host;
string thishost = ctx.Request.Url.Host;
if (refhost != thishost)
ctx.Response.AddHeader("Access-Control-Allow-Origin", string.Format("{0}://{1}", referrer.Scheme, referrer.Authority));
}
base.OnActionExecuting(actionContext);
}
}
You can decorate your controller's methods with that attribute and it will add the correct Access-Control-Allow-Origin for your caller's website, regardless of what that website is. And assuming that you're using IIS as your host... won't work for OWIN-hosted sites.
Example usage:
public class HelloController : ApiController
{
[AllowReferrer]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}