'Self Hosted WCF REST service JSON POST Method Not Allowed

I have literally seen thousands of these "WCF" questions on the internet so far, but I am beginning to think that it is impossible. Please someone tell me I am wrong...

Background: I am working with a Self Hosted WCF Service (therefore Global.asax.cs won't help here). Also the endpoints are defined programatically. The contract is decorated with WebInvoke(Method="POST") and I am making a JQuery call to the service.

The preflight works initially with the OPTIONS method but the POST method fails with 405 Method Not Allowed. Also GET functions work perfectly.

I have been searching the internet and experimenting for about a month now and it just will not budge. This service already responds fine to another client calling it through TCP... Please could some genius help me out. Thanks

PS: What I thought was really weird about the the POST response, is the Allow: OPTIONS... Surely that should not be there?

CORS

    public class CORSEnablingBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
        var requiredHeaders = new Dictionary<string, string>();

        requiredHeaders.Add("Access-Control-Allow-Origin", "*");
        requiredHeaders.Add("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");
        requiredHeaders.Add("Access-Control-Allow-Headers", "Origin, Cache-Control, Connection, Pragma, Content-Length, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, User-Agent");

        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new CORSHeaderInjectingMessageInspector(requiredHeaders));
    }

app.config

  <system.serviceModel>
<behaviors>
  <endpointBehaviors>
    <behavior name="SOAPDemoEndpointBehavior">
    </behavior>
    <behavior>
      <webHttp/>
      <crossOriginResourceSharingBehavior/>
    </behavior>
  </endpointBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<extensions>
  <behaviorExtensions>
    <add name="crossOriginResourceSharingBehavior" type="Application.Host.CORSEnablingBehavior, Application.Host, Version=1.0.0.0, Culture=neutral"/>
  </behaviorExtensions>
</extensions>
<bindings>
  <basicHttpBinding>
    <binding name="OrdersMappingSoap"/>
  </basicHttpBinding>

  <!--2015-08-26-->
  <webHttpBinding>
    <binding name="webHttpBindingWithJson"
          crossDomainScriptAccessEnabled="true" />
  </webHttpBinding>

Interface

[OperationContract(Name = "Relational")] 
[FaultContract(typeof(ValidationFault))]
[WebInvoke(Method = "POST", UriTemplate = "GetCustomerRelational", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped)]
CustomerFullModel GetCustomerRelational(int clientHandle, object customerID, bool loadRelationalData);

JQuery

jQuery.ajax({
  crossDomain: true,
  type: "POST",
  contentType: "application/json",
  url: "http://localhost:8086/CustomerService/rest/GetCustomerRelational/",
  data: JSON.stringify({
    "clientHandle": 1824,
    "customerID": "ABB029",
    "loadRelationalData": true
  }),
  dataType: "json",
  success: function(result) {
    console.log("Success...");
    document.getElementById("lblResponse").innerHTML = "Success: " + JSON.stringify(result.NormalResult);
  },
  error: function(x, s, t) {
    console.log("Error...");
    document.getElementById("lblResponse").innerHTML = x.responseText;
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Preflight Request

OPTIONS http://localhost:8086/CustomerService/rest/GetCustomerRelational/ HTTP/1.1
Host: localhost:8086
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36
Access-Control-Request-Headers: accept, content-type
Accept: */*
Referer: http://stacksnippets.net/js
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8

Preflight Response

HTTP/1.1 200 OK
Content-Length: 0
Server: Microsoft-HTTPAPI/2.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Origin, Cache-Control, Connection, Pragma, Content-Length, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, User-Agent
Date: Wed, 26 Aug 2015 13:13:59 GMT

POST Request

POST http://localhost:8086/CustomerService/rest/GetCustomerRelational/ HTTP/1.1
Host: localhost:8086
Connection: keep-alive
Content-Length: 69
Accept: application/json, text/javascript, */*; q=0.01
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36
Content-Type: application/json
Referer: http://stacksnippets.net/js
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
{"clientHandle":1824,"customerID":"ABB029","loadRelationalData":true}

POST Response

HTTP/1.1 405 Method Not Allowed
Allow: OPTIONS
Content-Length: 1565
Content-Type: text/html; charset=UTF-8
Server: Microsoft-HTTPAPI/2.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Origin, Cache-Control, Connection, Pragma, Content-Length, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, User-Agent
Date: Wed, 26 Aug 2015 13:14:02 GMT

<p>Method not allowed.</p>


Solution 1:[1]

I figured it out.

The main difference between this WCF and everyone else's, is the fact that mine is self hosted, while everything else is hosted on IIS (mostly).

Thanks to this article ASP.NET Compatibility Mode, the answer lies in the interception of the preflight request. IIS hosted WCF requires the interception be done in the global.asax file as follows:

protected void Application_BeginRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
        {
            //These headers are handling the "pre-flight" OPTIONS call sent by the browser
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE,OPTIONS");
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
            HttpContext.Current.Response.End();
        }
    }

This however, is not possible in self hosted WCF. BUT, we can still make use of ASP.NET functionality using this line in the app.config

<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

Then finally utilize this in the service class:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class TestService : ValidationModel, ITestService
{

I realized that it doesn't help just to set this to "Allowed", it must be "Required".

Final finally, in order for the preflight to start, the following code needs to be in the interface and the service:

[OperationContract]
    [FaultContract(typeof(ValidationFault))]
    [WebInvoke(Method = "OPTIONS", UriTemplate = "*")]
    void GetOptions();

public void GetOptions()
    {
        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK;
    }

Now, POST methods with complex parameters will pass the preflight test and execute the method with a response.

Solution 2:[2]

If you are dealing with this case in a self hosted service, the following procedure has worked for me:

  1. Add OPTIONS method into your Interface (the Browser, before calling your POST method, calls OPTIONS to verify the CORS)

    [OperationContract] [WebInvoke(Method = "OPTIONS", UriTemplate = "*")] void GetOptions();

  2. Implement in your ServiceBehavior class

    public void GetOptions()
    {           
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "POST");
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");    
    
    WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK;
    

    }

  3. The class may have to have this attribute:

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Kyle
Solution 2