I want to use CORS with multiple sources for my web application but I am stuck now. It worked with the first source because I set the Allow-Origin header from Web.config. But now I want to do it programmatically to change the header based on the Origin/Referer.
won't work in browsers like Chrome.
I created the Application_BeginRequest in Global.asax. Problem is...it is only triggered for GET/POST requests but not for the OPTIONS call.
Second option to add OnActionExecuting in a global filter. Same problem. OPTIONS requests don't trigger this method.
Any idea what is going around here?
I am using .NET Framework 4.8.
Code:
public class MyActionFilterAttribute : ActionFilterAttribute
{
private readonly string NEEDS_CUSTOM_ORIGIN = ConfigurationManager.ConnectionStrings["CustomOrigin"]?.ConnectionString ?? "";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.HttpMethod == "OPTIONS")
{
// IT NEVER HITS THIS OR ANY OTHER PART OF THIS METHOD FOR OPTIONS
}
if (!string.IsNullOrWhiteSpace(NEEDS_CUSTOM_ORIGIN))
{
var originHeader = "";
if (((IList)filterContext.HttpContext.Request.Headers.AllKeys).Contains("Origin"))
{
originHeader = filterContext.HttpContext.Request.Headers["Origin"];
}
else if (((IList)filterContext.HttpContext.Request.Headers.AllKeys).Contains("Referer"))
{
originHeader = filterContext.HttpContext.Request.Headers["Referer"];
}
if (!string.IsNullOrWhiteSpace(originHeader))
{
filterContext.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", originHeader);
}
}
}
}
Global.asax:
protected void Application_BeginRequest(object sender, EventArgs e)
{
if(Request.HttpMethod == "OPTIONS")
{
// NEVER
}
}
Well...after a few hours of digging I finally found the answer by comparing it with another project that seemed to work when adding the code above in it.
The fix in Web.config is the following and it looks like IIS is eager to handle the OPTIONS requests, so we need to remove the OPTIONSVerbHandler and then ours (the one from Application_BeginRequest) will be considered.
<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
And final code:
protected void Application_BeginRequest(object sender, EventArgs e)
{
var originHeader = "";
if (((IList)Request.Headers.AllKeys).Contains("Origin"))
{
originHeader = Request.Headers["Origin"];
}
else if (((IList)Request.Headers.AllKeys).Contains("Referer"))
{
originHeader = Request.Headers["Referer"];
}
if (!string.IsNullOrWhiteSpace(originHeader))
{
Response.Headers.Add("Access-Control-Allow-Origin", originHeader);
}
if (Request.HttpMethod == "OPTIONS")
{
Response.Flush();
}
}
Related
I am trying to update my resource from Angular app, making Web Api call using PUT method. For some reason, all I receive is 400 Bad Request error, even when I try to do the same using Postman.
Therefore I have two questions, first - if the following code is correct, taking into account web.config file, and console output, and second - should I configure my IIS somehow to allow PUT calls ? Everything is running on IIS, and so far I've met with blogpost while working on this problem, which mention something like this - I don't really know what should be changed. cause it seems to me that I've removed WebDAV in my web.config.
web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule"/>
</modules>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
<remove name="WebDAV" />
</handlers>
<aspNetCore processPath=".\AuctorAPI.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
</system.webServer>
</location>
</configuration>
update method in my angular service:
updateClient(client: any) {
console.log(client);
var obj = JSON.parse(client);
const headers = new HttpHeaders().set('content-type', 'application/json');
var body = {
name: obj['name'],
surname: obj['surname'],
phone: obj['phone'],
email: obj['email'],
gymEntriesLeft: obj['gymEntriesLeft'],
martialArtsEntriesLeft: obj['martialArtsEntriesLeft']
}
console.log("ID");
console.log(obj['id']);
console.log("BODY");
console.log(JSON.stringify(body));
return this.http.put<Client>(this.url + obj['id'], JSON.stringify(body), { headers }).pipe(
catchError(this.errorHandler)
);
}
Angular component (calling delete method)
onFormSubmit() {
this.clientService.updateClient( this.clientById).subscribe(() => {
this.getClients();
this.edit = false;
this.editRowId = null;
})
}
Controller method:
// PUT: api/Clients/5
[HttpPut("{id}")]
public async Task<IActionResult> PutClient(int id, Client client)
{
if (id != client.Id)
{
return BadRequest();
}
_context.Entry(client).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ClientExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
It seems that you simply have to pass the client id to the request body as that's what's being checked in the action:
var body = {
id: obj['id'],
name: obj['name'],
surname: obj['surname'],
phone: obj['phone'],
email: obj['email'],
gymEntriesLeft: obj['gymEntriesLeft'],
martialArtsEntriesLeft: obj['martialArtsEntriesLeft']
}
Solution has been found!
The problem was that in body there was no ID passed to the PUT action. I debugged it in Visual Studio, by sending Postman request, and as it clearly is written in a code - bad request is returned only when passed ID differs from object's ID. Thank you #AndriiLitvinov, I just discovered that when you posted the comment, but your solution was a favour!
I am working on building an API with ASP.NET/C#. However, when attempting to make either a PUT or DELETE request, I receive the following error:
"The requested resource does not support http method PUT (or DELETE)"
I realize this issue has been discussed before; however, I have looked over the responses (including this one) to related questions but have yet to find the solution. I have disabled WebDAV and ensured that the verbs are allowed in the ExtensionlessUrlHanlder. The "webserver" portion of my web.config is as follows:
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
<remove name="WebDAVModule"/>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
</modules>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
</customHeaders>
</httpProtocol>
<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="WebDAV" />
<remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
<validation validateIntegratedModeConfiguration="false" />
</system.webServer>
The controller is as follows:
namespace MidamAPI.Controllers
{
[RoutePrefix("SupplyItems")]
public class SupplyItemsController : ApiController
{
UnitOfWork worker = new UnitOfWork();
[Route("")]
[HttpGet]
public string Get()
{
IEnumerable<SupplyItemsDTO> dtoList = Mapper.Map<List<SupplyItem>, List<SupplyItemsDTO>>(worker.SupplyItemRepo.Get().ToList());
return JsonConvert.SerializeObject(dtoList);
}
[Route("{propertyType}")]
public string Get(String propertyType = "BK")
{
IEnumerable<SupplyItemsDTO> dtoList = null;
if (propertyType.Equals("POPEYES", StringComparison.CurrentCultureIgnoreCase))
{dtoList = Mapper.Map<List<PopeyesSupplyItem>, List<SupplyItemsDTO>>(worker.PopeyesItemRepo.Get().ToList());
}
dtoList = Mapper.Map<List<BKSupplyItem>, List<SupplyItemsDTO>>(worker.BKItemRepo.Get().ToList());
return JsonConvert.SerializeObject(dtoList);
}
[Route("{id:int}")]
public string Get(int id)
{
SupplyItemsDTO dto = Mapper.Map<SupplyItem, SupplyItemsDTO>(worker.SupplyItemRepo.GetByID(id));
return JsonConvert.SerializeObject(dto);
}
[Route("")]
[HttpPost]
public HttpResponseMessage Post([FromBody]SupplyItem itm)
{
try
{
worker.SupplyItemRepo.Insert(itm);
worker.Save();
return Request.CreateResponse(HttpStatusCode.OK, itm);
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, ex);
}
}
[Route("")]
[HttpDelete]
public HttpResponseMessage Delete(int id)
{
try
{
SupplyItem itm = worker.SupplyItemRepo.GetByID(id);
worker.SupplyItemRepo.Delete(itm);
worker.Save();
return Request.CreateResponse(HttpStatusCode.OK, itm);
}
catch(Exception ex)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, ex);
}
}
[Route("")]
[HttpPut]
public HttpResponseMessage Put(int id, SupplyItem item) {
try
{
item.ID = id;
worker.SupplyItemRepo.Update(item);
return Request.CreateResponse(HttpStatusCode.OK, item);
}
catch(Exception ex)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, ex);
}
}
}
}
The GET and POST calls work as expected. I do not want to alter the applicationhost.config file, really modify the web server in any way (since this is my development machine), or use headers that might represent a security vulnerability.
The response headers:
Access-Control-Allow-Credentials →true
Access-Control-Allow-Headers →Origin, X-Requested-With, Content-Type, Accept, X-Token,Authorization
Access-Control-Allow-Methods →GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Methods →GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Origin →*
Allow →GET
Cache-Control →no-cache
Content-Length →72
Content-Type →application/json; charset=utf-8
Date →Tue, 10 Oct 2017 13:39:03 GMT
Expires →-1
Pragma →no-cache
Server →Microsoft-IIS/8.0
X-AspNet-Version →4.0.30319
X-Powered-By →ASP.NET
X-SourceFiles →=?UTF-8?B?QzpcVmlzdWFsIFN0dWRpbyBQcm9qZWN0c1xNaWRhbWVyaWNhQVBJXE1pZGFtQVBJXHN1cHBseWl0ZW1zXDQ1NA==?=
The request in the IIS logs:
2017-10-10 13:27:35 ::1 PUT /supplyitems/454 - 57263 - ::1 PostmanRuntime/6.3.2 - 405 0 0 0
My routing:
config.Routes.MapHttpRoute(
name: "DefaultAPI",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Any advice is appreciated. I am using IIS Express. Thanks.
You might still have some extra modules installed. Even if you get those fixed, your request and routes won't match.
Your route config is being ignored as you have explicitly set the routes using your route attributes. You should update your DELETE method route attribute to [Route("{id:int}")]. For the PUT method, it is a little more unclear as to what you are doing, but I would assume you would want to do something like this:
[Route("{id:int}")]
[HttpPut]
public HttpResponseMessage Put([FromUrl]int id, [FromBody]SupplyItem item) {
...
The adventure with that error for me was:
You are getting that probably because you have some extra IIS modules installed:
WebDAV If you don't need it I would just remove it from your IIS.
This module is part of IIS Roles.
To get rid of that go to:
Windows Features -> Turn Windows Features On/Off -> Internet Information Services -> World Wide Web Services -> Common HTTP Features -> WebDAV Publishing.
If you want to keep it on, you need to add PUT/DELETE to ISAPI filters I think.
More explanation about here: https://stackoverflow.com/a/33552821/4869329
i am trying to add CORS setting by enabling "Access-Control-Allow-Origin" header in Web Service method shown below. However, I'm still getting error: No 'Access-Control-Allow-Origin' header is present on the requested resource.' Am i missing anything?
[ScriptMethod(UseHttpGet = true)]
[WebMethod]
public ClientData[] GetClientData(int Number)
{
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "http://localhost:52630");
ClientData[] Clients = null;
if (Number > 0 && Number <= 10)
{
Clients = new ClientData[Number];
for (int i = 0; i < Number; i++)
{
Clients[i].Name = "Client " + i.ToString();
Clients[i].ID = i;
}
}
return Clients;
}
Put this in your web.config
You can adjust if needed. This example I only open for .aspx
<configuration>
<system.web>
<httpHandlers>
<add verb="GET,HEAD,POST,OPTIONS" path="*.aspx" type="System.Web.UI.PageHandlerFactory" />
</httpHandlers>
</system.web>
</configuration>
You may need something like this too.
if (Request.HttpMethod == "OPTIONS")
{
Response.AppendHeader("Access-Control-Allow-Origin", "*");
Response.AppendHeader("Access-Control-Allow-Headers", "Content-Type");
return;
}
I'm working on a CompressionModule to be used in web.config, I've broken up a bigger issue into this case which I'm stumped on.
For this example I've created a new MVC4 internet application and have made the following modifications to web.config:
<handlers>
<!--
<remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
<remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
<add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
-->
<add name="Content" path="*" verb="*" preCondition="integratedMode" type="System.Web.StaticFileHandler" />
</handlers>
<modules>
<remove name="monorailRouting" />
<add name="compressionModule" type="Platform.Web.CompressionModule" />
<!--
<add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
-->
</modules>
Where compression module is
namespace Platform.Web {
public class CompressionModule : IHttpModule {
#region HTTP Header Constants
private const string DEFLATE = "deflate";
private const string GZIP = "gzip";
private const string CONTENT_ENCODING = "Content-Encoding";
private const string ACCEPT_ENCODING = "Accept-Encoding";
private const string VARY = "Vary";
#endregion
#region IHttpModule Members
public void Dispose() {
//noop
}
public void Init(HttpApplication context) {
context.BeginRequest += new EventHandler(context_CompressResponse);
}
void context_CompressResponse(object sender, EventArgs e) {
HttpApplication app = (HttpApplication)sender;
string encodings = app.Request.Headers.Get(ACCEPT_ENCODING);
if (encodings == null)
{
return;
}
encodings = encodings.ToLower();
if (encodings.Contains(GZIP))
{
//1
app.Response.Filter = new GZipStream(app.Response.Filter, CompressionMode.Compress);
//2
app.Response.AppendHeader(CONTENT_ENCODING, GZIP);
}
app.Response.AppendHeader(VARY, CONTENT_ENCODING);
}
#endregion
}
}
And I am experiencing the following issues:
If line 1 and 2 exist the app loads fine. But nothing is apparently
gzipped in the headers, nor the body. I'm determining this through Fiddler.
If line 1 exists but 2 doesn't the app loads but unreadable gzip is
displayed in the browser.
If line 1 doesn't exists but 2 does the app fails to load but
Content-Encoding: gzip is in the headers.
Anyone have some suggestions on what I may be doing wrong?
Two things I believe should be done here are,
the response filter should be taken into a copy of stream
the vary by headers has to be changed
Try the following code:
void context_CompressResponse(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
string encodings = app.Request.Headers.Get(ACCEPT_ENCODING);
if (encodings == null)
{
return;
}
encodings = encodings.ToLower();
if (encodings.Contains(GZIP))
{
Stream baseStream = app.Response.Filter;
//1
app.Response.Filter = new GZipStream(baseStream, CompressionMode.Compress);
//2
app.Response.AppendHeader(CONTENT_ENCODING, GZIP);
}
Response.Cache.VaryByHeaders[ACCEPT_ENCODING] = true;
}
Also, for further discussions, please look into this SO article
Turns out Fiddler2 isn't reading the headers correctly.
If I view the response in Chrome I can see that Content-Encoding is being set to gzip.
-Geoff
I am trying to do a CORS call to a WCF service endpoint hosted on IIS7.5.
I have configured custom headers in IIS. My configuration looks like below
<customHeaders>
<add name="Access-Control-Allow-Methods" value="GET,PUT,POST,DELETE,OPTIONS" />
<add name="Access-Control-Allow-Headers" value="x-user-session,origin, content-type, accept" />
<add name="Access-Control-Allow-Credentials" value="true" />
</customHeaders>
When I do a POST request I get following error message
"Request header field x-user-session is not allowed by Access-Control-Allow-Headers"
If I remove my custom header from the call and run it, everything works fine.
Also if I do a GET call with custom header then also API works correctly.
$.ajax({
type:"POST",
success: function(d) { console.log(d) },
timeout: 9000,
url: "http://api.myserver.com/Services/v2/CreditCard.svc/update_cc_detail",
data: JSON.stringify({"card_id": 1234,"expire_month":"11","expire_year":"2020","full_name":"Demo Account", "number":"4111111111111111","is_primary":true}),
xhrFields: { withCredentials: true},
headers: { x-user-session': "B23680D0B8CB5AFED9F624271F1DFAE5052085755AEDDEFDA3834EF16115BCDDC6319BD79FDCCB1E199BB6CC4D0C6FBC9F30242A723BA9C0DFB8BCA3F31F4C7302B1A37EE0A20C42E8AFD45FAB85282FCB62C0B4EC62329BD8573FEBAEBC6E8269FFBF57C7D57E6EF880E396F266E7AD841797792619AD3F1C27A5AE" },
crossDomain: true,
contentType: 'application/json'
});
UPDATES
Following is a link to the FireBug log
https://gist.github.com/anonymous/7333130
Most probably the issue is that the OPTIONS request is returning an error (404 or 405). This would explain why GET works properly (it doesn't always require preflight request) while POST don't (it always require preflight).
Usually the easiest solution for WCF services hosted on IIS is to enable all methods through WebInvokeAttribute:
[WebInvoke(Medthod="*")]
...
Sometimes the IIS is blocking the OPTION request with its default OPTIONSVerbHandler. You can remove this handler through your web.config:
<handlers>
<remove name="OPTIONSVerbHandler"/>
</handlers>
In worst case scenario (when the OPTIONS is not blocked by IIS but can't be handled by WCF) you can set your own handler. In such scenario first you need to a simple class like the one below in an assembly:
namespace CustomHandlers
{
public class CORSOPTIONSVerbHandler : IHttpHandler
{
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
if (context.Response.HttpMethod == "OPTIONS")
context.Response.StatusCode = 200;
else
context.Response.StatusCode = 405;
context.Response.End();
}
}
}
The new handler can be added in web.config like this:
<handlers>
<remove name="OPTIONSVerbHandler"/>
<add name="CORSOPTIONSVerbHandler" verb="OPTIONS" path="*" type="CustomHandlers.CORSOPTIONSVerbHandler, CustomHandlers"/>
</handlers>
Of course you need to adjust the assembly, namespace and class name properly.
Try adding these to your web.config
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, Content-Type, Accept" />
It usually happens because of cross-origin requests
I had a similar problem but with service stack, and to properly make your service to answer to the OPTIONS request, you need to explicitly put by yourself the headers. Maybe in WCF is not necessary, but I would try.
Using tpeczek's code:
namespace CustomHandlers
{
public class CORSOPTIONSVerbHandler : IHttpHandler
{
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
if (context.Response.HttpMethod == "OPTIONS")
context.Response.StatusCode = 200;
context.Response.Headers.Add("Access-Control-Allow-Headers" ,"x-user-session,origin, content-type, accept");
else
context.Response.StatusCode = 405;
context.Response.End();
}
}
}
I've not tried the code, it may not run, but I hope the idea is clear :)