3

As part of my work environment we need to support IE8, but would like to move forward with technology, specifically CORS.

I'm having trouble posting complex objects to a cors service in ie8. The object is null. Below are the steps to reproduce. If needed i can upload the project to github.

I've created a new mvc4 project. Added a API Controller. And made the following changes.

To Support preflight complex cors calls (global.asax):

    protected void Application_BeginRequest()
    {
        //This is needed for the preflight message
        //https://stackoverflow.com/questions/13624386/handling-cors-preflight-requests-to-asp-net-mvc-actions
        if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")  {  Response.Flush(); }
    }

Source: Handling CORS Preflight requests to ASP.NET MVC actions

To Support text/plain (ie8 only sends text/plain with cors)(global.asax):

    protected void Application_Start()
    {
        //This is needed to support text/plain
        HttpConfiguration config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
        config.Formatters.Remove(config.Formatters.FormUrlEncodedFormatter);
        config.Formatters.Remove(config.Formatters.XmlFormatter); 

        ...
    }

Credit: Posting text/plain as a complex object in WebAPI with CORS

To Support additional function names other than just verbs (put/post/etc) (WebApiConfig.cs)"

    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "APICustom",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        ...
    }

To support cors (web.config)

<httpProtocol>
   <customHeaders>
     <!-- cors -->
     <add name="Access-Control-Allow-Origin" value="*" />
     <add name="Access-Control-Allow-Headers" value="Content-Type" />
   </customHeaders>
</httpProtocol>

API Controller, I called PersonController.cs

 public class PersonController : ApiController
{

    public List<string> Get()
    {
        List<string> s = new List<string>();
        s.Add("s");
        s.Add("t");
        s.Add("u");
        return s;
    }



    [Serializable()]
    public class BaseReply
    {
        public bool successful = true;
        public string error;
    }
    [Serializable()]
    public class UpdateSomethingReply:  BaseReply
    {
        public UpdateSomethingRequest request;
        public List<string> stuff = new List<string>();
    }
    [Serializable()]
    public class UpdateSomethingRequest
    {
        public int hasInt;
        public string hasString;
    }
    //[FromBody] 
    [HttpPost]
    public UpdateSomethingReply UpdateSomething([FromBody] UpdateSomethingRequest request)
    {
        string body = Request.Content.ReadAsStringAsync().Result;
        UpdateSomethingReply reply = new UpdateSomethingReply();
        reply.request = request;

        reply.stuff.Add("v");
        reply.stuff.Add("w");
        reply.stuff.Add("x");
        return reply;
    }

That is the extent on the changes on the service. So next I create a client. This is also an mvc4 project. Pretty basic stuff here.

To polyfill ie8 with cors (index.cshtml):

<script src="~/Scripts/jQuery.XDomainRequest.js"></script>

Source: https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest

To call the cors service

 $(document).ready(function () {
        $.when(
          $.ajax({
              url: urls.person.UpdateSomething,
              type: 'post',
              contentType: "application/json; charset=utf-8",
              dataType: 'json',
              data: JSON.stringify({
                  hasInt: 1,
                  hasString: "u"
              })
          })
        )
        .fail(function (jqXHR, textStatus, errorThrown) {
        })
        .done(function (data) {
            console.log(JSON.stringify(data));
        });

        $.when(
          $.ajax({
              url: urls.person.Get,
              dataType: 'json'
          })
        )
        .fail(function (jqXHR, textStatus, errorThrown) {
        })
        .done(function (data) {
            console.log(JSON.stringify(data));
        });

        $.when(
          $.ajax({
              url: urls.person.UpdateSomething,
              type: 'post',
              contentType: "text/plain",
              dataType: 'json',
              data: JSON.stringify({
                  hasInt: 1,
                  hasString: "u"
              })
          })
        )
        .fail(function (jqXHR, textStatus, errorThrown) {
        })
        .done(function (data) {
            console.log(JSON.stringify(data));
        });
    });

As i stated earlier all 3 calls complete in ie8. But the request object in the service is null in ie8 and in firefox it is populated, even when i force the content-type to be text/plain

IE8 Console Output:

{"request":null,"stuff":["v","w","x"],"successful":true,"error":null}

Firefox Console Output:

{"request":{"hasInt":1,"hasString":"u"},"stuff":["v","w","x"],"successful":true,"error":null}

Update 9/25/2013

I can confirm that the body is being sent, but isn't being parsed by web api. If I add the following hack it will return the data as expected. In firefox the body will be empty and the request object is populated. In ie8 the body still contains the contents and the request is null.

    [HttpPost]
    public UpdateSomethingReply UpdateSomething(UpdateSomethingRequest request)
    {
        if (request == null && Request.Content.ReadAsStringAsync().Result !="")
        {
            request = JsonConvert.DeserializeObject<UpdateSomethingRequest>(Request.Content.ReadAsStringAsync().Result);
       }

        UpdateSomethingReply reply = new UpdateSomethingReply();
        reply.request = request;
        reply.body=Request.Content.ReadAsStringAsync().Result;
        reply.headers = Request.Headers.ToString();
        reply.stuff.Add("v");
        reply.stuff.Add("w");
        reply.stuff.Add("x");
        return reply;
    }
Community
  • 1
  • 1
Todd Horst
  • 853
  • 10
  • 22
  • Makes me wonder if this was one of the reasons jQuery decided not to make an exception for IE to make CORS requests work in IE without a patch. – Kevin B Sep 24 '13 at 18:51
  • 1
    Note that the inferred content type of the request is not text/plain in the absence of a Content-Type header. Per the spec, it is application/octet-stream, although the server may determine otherwise based on the resource properties contained in the URI, for example. I realize that the MSDN blog on XDomainRequest claims that the Content-Type is restricted to text/plain, but this is incorrect. The mail RFC says that Content-Type defaults to text/plain, but HTTP (RFC 2616) says application/octet-stream. – Ray Nicholus Sep 24 '13 at 19:17
  • @kev- definitely, cors has limitations on ie8. But in this case i believe its fine, instead i think its web api thats the issue, given that it returns data, just doesn't parse the request – Todd Horst Sep 24 '13 at 21:26
  • @ray thanks, ill check it, but i think it is coming across as text, because if i remove my global asax code for text plain it errors, but maybe i just tested it in firefox. I will verify – Todd Horst Sep 24 '13 at 21:29
  • I wouldn't expect any framework to automagically parse any request body sent by XDomainRequest, due to the lack of a Content-Type header. – Ray Nicholus Sep 24 '13 at 21:32
  • The code i have in global asax is to treat any request with text/plain as if it were sent in with json headers. With json headers web api normally parses the request into the object defined in the function. You could be right, it may either not have the content type, or it default to something else. But im not at my desk. – Todd Horst Sep 24 '13 at 21:51
  • If you are sending the request in IE9 or older, cross-origin, then there is no Content-Type header. – Ray Nicholus Sep 24 '13 at 21:55
  • Hey, I'm working on the same problem. I just replied to Todd's answer with the current solution I'm attempting to implement. hope it helps. – Robert Christ Sep 29 '13 at 21:00

2 Answers2

3

here's the code I was talking about. Create this as a new class, I created a DelegatingHandlers folder in my WebAPI project (but then again, I also have a filters folder, A model bindings folder...)

I've included TONS of comments that you could easily remove.

The below assumes IE 8/9 will always be sending "JSON" data. If your webAPI implementation allows content negotiation, and you want to include that feature for IE8/9 then you will obviously need to add a few if statements to the below code, but this should be more than enough to get you going. I personally just stated that I only accept JSON from IE 8/9.

namespace REDACTED.WebApi.DelegatingHandlers
{
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Threading;
    using System.Threading.Tasks;

    /// <summary>
    /// Gives the WebAPI the ability to handle XDomainRequest objects with embedded JSON data.
    /// </summary>
    public class XDomainRequestDelegatingHandler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // XDomainRequest objects set the Content Type to null, which is an unchangable setting.
            // Microsoft specification states that XDomainRequest always has a contenttype of text/plain, but the documentation is wrong.
            // Obviously, this breaks just about every specification, so it turns out the ONLY extensibility
            // point to handle this is before the request hits the WebAPI framework, as we do here.

            // To read an apology from the developer that created the XDomainRequest object, see here: 
            // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx

            // By international specification, a null content type is supposed to result in application/octect-stream (spelling mistake?),
            // But since this is such an edge case, the WebAPI framework doesn't convert that for us before we hit this point.  It is unlikely, 
            // but possible that in a future Web.Api release, we will need to also sniff here for the octect header.
            if (request.Content.Headers.ContentType == null)
            {
                request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            }

            return base.SendAsync(request, cancellationToken);
        }
    }
}

My WebAPIConfig file looks as follows:

        public static void Register(HttpConfiguration config)
        {
             // Normal config.Routes statements go here

            // Deserialize / Model Bind IE 8 and 9 Ajax Requests
            config.MessageHandlers.Add(new XDomainRequestDelegatingHandler());
        }

Then to make sure my POST calls were IE 8 and 9 compliant, in my JS I put the following (though obviously you only need to include this if you are also consuming your own API)

esbPost: function (apiUrl, apiData, fOnSuccess, fOnFailure) {
    $.support.cors = true; // Not sure that I need this.

    var testModernAjax = function () {
        if (window.XMLHttpRequest) {
            var testRequest = new XMLHttpRequest;

            // IE 8 / 9 with jQuery can create XMLHttpRequest objects, but only modern 
            // CORS implementing browsers (everything + IE10) include the withCredentials specification.
            if ('withCredentials' in testRequest) {
                return true;
            }
            return false;
        }
        return false;
    };

    var testMsieAjax = function () {
        if (window.XDomainRequest) {
            return true;
        }
        return false;
    };

    //All browsers, and IE 10
    if (testModernAjax()) {
        $.ajax({
            url: apiUrl,
            type: 'POST',
            dataType: 'json',
            data: apiData,
            success: function (result) {
                if (fOnSuccess) {
                    fOnSuccess(result);
                }
            },
            error: function (jqXHR, textStatus, errorThrown) {
                if (fOnFailure) {
                    fOnFailure(jqXHR, textStatus, errorThrown);
                }
            }
        });
    //IE 8 / 9
    } else if (testMsieAjax()) {
        var xdr = new XDomainRequest();
        xdr.onload = function () {
            var parsedResponse = $.parseJSON(xdr.responseText);
            if (fOnSuccess) {
                fOnSuccess(parsedResponse);
            }
        };
        xdr.onerror = function () {
            if (fOnFailure) {
                fOnFailure();
            }
        };
        xdr.onprogress = function () { };
        xdr.open("post", apiUrl);
        xdr.send(JSON.stringify(apiData));
    } else {
        // IE 7 can only do AJAX calls through a flash/iframe exploit, earlier do not include ajax support.
        throw new 'This browser is unsupported for this solution.';
    }
},

Personally, I'm using JSONP for GETs, and not using PUTS or DELETES whatsoever, so that's sufficient for me. If I were to do this project over again, I would use PUTS and DELETES. To make IE 8 / 9 handle cross domain PUTS and DELETES its apparently common practice to include a new node on the data being sent, or in the header, called some variant of "Type", and use a string "PUT" or "DELETE". I'm not sure where I'd sniff that out though.

Enabling CORS was as easy as putting the following in the Web.Config.

<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <!--<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />-->
      </customHeaders>
    </httpProtocol>

As you can see in the above comment, you can also restrict CORS by originating url (the *) and the type of request (put, post, etc). Totally makes stuff like this completely unnecessary. This guy's blog gives a really good walkthrough.

And that's literally all you need to do to a brand new WebAPI project to make it Support both CORS and IE 8/9.

Robert Christ
  • 1,910
  • 2
  • 13
  • 19
  • 1
    +1 for the Delegating handler code. This allowed me to handle IE8/9 CORS POST's correctly. I used this jQuery shim http://api.brain-map.org/examples/doc/scatter/javascripts/jquery.ie.cors.js.html which worked fine. – Brad Cunningham Mar 11 '14 at 18:45
0

Until I can find another solution or we can stop supporting IE8 here is the accepted hack. Credit to a coworker for coming up with this.

  1. Remove support for text/plain in global.asax, the headers sent by ie8 are all null. As discussed in the comments, the request body isn't automatically parsed. The contents remain in the body. Normally (say in Firefox) the body is parsed into the request object and replaced with an empty string.
  2. In App_Start create a class called GenericBinder

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    
    using Newtonsoft.Json;
    using System.Web.Http.Controllers;
    using System.Web.Http.ModelBinding;
    namespace Admin2
    {
      public class GenericBinder : IModelBinder
      {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            bindingContext.Model = JsonConvert.DeserializeObject(actionContext.Request.Content.ReadAsStringAsync().Result, bindingContext.ModelType);
            return true;
        }
      }
    }
    
  3. Change person controller as follows

    using System.Web.Http.ModelBinding;
    
    ...
    
    [HttpPost]
    public UpdateSomethingReply UpdateSomething([ModelBinder(typeof(GenericBinder))] UpdateSomethingRequest request)
    {
      UpdateSomethingReply reply = new UpdateSomethingReply();
      reply.request = request;
      reply.stuff.Add("v");
      reply.stuff.Add("w");
      reply.stuff.Add("x");
      return reply;
    }
    

IE8 now has the ability to send in complex data.

Todd Horst
  • 853
  • 10
  • 22
  • I really didn't want to go this route, because I was already using FromUri and FromBody binders all over my code, and adding a new model binding type to every single endpoint seemed frought with difficulties. Even worse, in your example code here, there's no way to say "If the request has a content type specified, Use default binding", because WebAPI has no real default binding, just a bunch of provider factories under the hood. – Robert Christ Sep 29 '13 at 20:58
  • Instead what I ended up doing was creating a new custom delegate handler, and sniffing for incoming requests that are missing the contenttype. If so, I update the content type to what I think it should be, and then when the request hits the WebAPI framework, it automatically parses it according to what I set. As a result, the rest of my code doesn't even know IE was being a problem. Dennis Puzak has a simple example here: http://stackoverflow.com/questions/18964258/asp-web-api-post-request-with-cors-and-ie9-xdomainrequest-object – Robert Christ Sep 29 '13 at 21:00
  • Could you post your answer with code so i can try it out, and mark it as the accepted answer – Todd Horst Sep 30 '13 at 21:13