20

I'm trying to allow POST requests from my javascript app hosted at localhost:80 to a WCF REStful service hosted at a different port, but somehow it doesn't work. I've tried adding custom properties to the header, as well as adding it programatically in my service's JSONData method but I'm still getting '405 Method not allowed' in my response. What is the proper approach here ?

This is my interface :

namespace RestService
{
    public class RestServiceImpl : IRestServiceImpl
    {
        #region IRestServiceImpl Members

        public string JSONData()
        {
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
            return "Your POST request";
        }

        #endregion
    }
}

and the service code :

using System.ServiceModel;
using System.ServiceModel.Web;
using System.Web.Script.Services;

namespace RestService
{

    [ServiceContract]
    public interface IRestServiceImpl
    {
        [OperationContract]
        [ScriptMethod]
        [WebInvoke(Method = "POST",
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            UriTemplate = "export")]
        string JSONData();
    }
}

And finally the config :

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="RestService.RestServiceImpl" behaviorConfiguration="ServiceBehaviour">
        <endpoint address ="" binding="webHttpBinding" contract="RestService.IRestServiceImpl" behaviorConfiguration="web">
        </endpoint>
      </service>
    </services>

    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehaviour">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <httpProtocol>
      <customHeaders>
         <add name="Access-Control-Allow-Origin" value="*" />
      </customHeaders>
</httpProtocol>  
  </system.webServer>

</configuration>
mike_hornbeck
  • 1,612
  • 3
  • 30
  • 51

5 Answers5

34

This worked better for me than the Web.config version:

Create a Global.asax

Add this method to the Global.asax.cs:

using System.Web;

namespace StackOverflow
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
            if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
                HttpContext.Current.Response.End();
            }
        }
    }
}

Ref: http://www.dotnet-tricks.com/Tutorial/wcf/X8QN260412-Calling-Cross-Domain-WCF-Service-using-Jquery.html

Akira Yamamoto
  • 4,685
  • 4
  • 42
  • 43
18

Add these nodes to your Web.config:

<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*"/>
        <add name="Access-Control-Allow-Headers" value="Content-Type, Accept" />
        <add name="Access-Control-Allow-Methods" value="POST,GET,OPTIONS" />
        <add name="Access-Control-Max-Age" value="1728000" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Ref: http://theagilecoder.wordpress.com/2014/07/07/wcf-and-cors-no-access-control-allow-origin-header-is-present-on-the-requested-resource/

Akira Yamamoto
  • 4,685
  • 4
  • 42
  • 43
9

Enabling CORS for non-GET requests requires more than just setting the Access-Control-Allow-Origin header - it also needs to deal with preflight requests, which are OPTIONS requests which ask the server whether it's safe to perform operations which can potentially change data (e.g., POST, PUT, DELETE) before the actual request is sent.

I've written a blog post about adding CORS support for WCF. It's not the simplest of the implementations, but hopefully the code in the post can be simply copied / pasted into your project. The post can be found at http://blogs.msdn.com/b/carlosfigueira/archive/2012/05/15/implementing-cors-support-in-wcf.aspx.

carlosfigueira
  • 85,035
  • 14
  • 131
  • 171
  • That's almost what I need. But I've noticed that it doesn't work if you send the data as a json. Your exabple page sends a single string value with the POST request. Or maybe it's because ExtJS handles the request data differently than jQuery :/ – mike_hornbeck Dec 27 '12 at 11:25
  • The example does send data as JSON - the inputs to the POST / PUT methods are *JSON strings* (notice that the input is wrapped in `"`'s). It works for objects as well, it's just that in the example the operation takes a string as a parameter. – carlosfigueira Dec 27 '12 at 15:53
  • I've changed the data in the test page to ` var data = { foo: "bar" };` and I'm getting 400 Bad request. I've checked the logs but nothing helpful there. So should I make changes also in the WCF itself to support it ? – mike_hornbeck Dec 27 '12 at 17:03
  • Yes - in that case the operation must take a parameter of a user type, with a string field named 'foo'. If the operation takes a string, you should pass a string; you can only pass an object if the operation takes an object as a parameter. – carlosfigueira Dec 27 '12 at 17:32
  • I've tried changing argument type to Object but the same happens. Have you tried your example with objects in place of strings ? Though JSON is stringified anyway so this should work with a string. Or am I wrong? – mike_hornbeck Dec 27 '12 at 22:21
  • 1
    It shouldn't be `Object`, but a data type which maps to the JSON you want to send. For example, if your class has a string property called "Foo", it should be able to accept the JSON object `{"Foo":"hello world"}` as input. – carlosfigueira Dec 27 '12 at 22:33
  • Ok, it was enough to just assign each property from the json to a new argument for the operation. – mike_hornbeck Dec 28 '12 at 17:21
0

The following .NET code (global.asax) has an important difference that in stead of *, it can be better to echo back the Origin domain because this enables authentication over CORS (e.g. NTLM / Kerberos) as well as the Preflight.

void Application_BeginRequest(object sender, EventArgs e)
{
    if (Request.HttpMethod == "OPTIONS")
    {
        Response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
        Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
        Response.AddHeader("Access-Control-Max-Age", "1728000");
        Response.End();
    }
    else
    {
        Response.AddHeader("Access-Control-Allow-Credentials", "true");

        if (Request.Headers["Origin"] != null)
            Response.AddHeader("Access-Control-Allow-Origin" , Request.Headers["Origin"]);
        else
            Response.AddHeader("Access-Control-Allow-Origin" , "*");
    }
}
QA Collective
  • 2,222
  • 21
  • 34
0

All of the solutions above modify the CORS response to allow all sites to be CORS enabled by using the * attribute. This seemed like a security risk to me as I wanted to control what sites were given access to my REST services. I am hoping this may help others with the same type of issue.

I started by modifying the web.config file to include my allowed origin sites by using the SpecializedString collection which allows a string array to be stored in MySettings. The code is in VB.Net so should port easily to c#, if desired.

<applicationSettings>
    <YourService.My.MySettings>
        <setting name="AllowedOrigins" serializeAs="Xml">
            <value>
                <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                    <string>http://localhost:62087</string>
                    <string>https://yourallowedorign2:3344</string>
                </ArrayOfString>
            </value>
        </setting>
    </YourService.My.MySettings>
</applicationSettings>

And in the Global.asax.vb file, I made the following adjustments to the Application_BeginRequest code.

Note the support function to get the Allowed Origins

Private allowedOrigins As String()

Public Function GetAllowedOrigins() As String()
    Dim mySetting As StringCollection = My.Settings.AllowedOrigins

    If mySetting IsNot Nothing Then
        ' If you're using .NET 3.5 or greater:
        Return mySetting.Cast(Of String)().ToArray()

        ' Otherwise:
        Dim array(mySetting.Count - 1) As String
        mySetting.CopyTo(array, 0)

        Return array
    Else
        Dim strEmpty() As String = Enumerable.Empty(Of String).ToArray
        Return strEmpty
    End If
End Function

Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
    ' Fires at the beginning of each request
    Dim origin As String = sender.Request.Headers("Origin")
    If origin IsNot Nothing Then
        Dim originURI As Uri = New Uri(origin)
        Dim requestHost As String = originURI.Scheme + Uri.SchemeDelimiter + originURI.Host
        If originURI.Port <> 80 Then
            requestHost += ":" + originURI.Port.ToString
        End If

        If allowedOrigins Is Nothing Then
            allowedOrigins = GetAllowedOrigins()
        End If
        If allowedOrigins IsNot Nothing AndAlso allowedOrigins.Contains(requestHost) Then
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", requestHost)
            If HttpContext.Current.Request.HttpMethod = "OPTIONS" Then
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With")
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000")
                HttpContext.Current.Response.End()
            End If
        End If
    End If

End Sub

One other caveat that I found is that I needed to add the X-Requested-With value in the Access-Control-Allow-Headers header. If you get a CORS error, check that header first to see if you need additional options.

I hope this helps other that may be frustrated by this all too common issue.