Cors, Web Api, IE8, Post Complex Data

2019-04-01 01:12发布

问题:

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;
    }

回答1:

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.



回答2:

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.