Cors,Web Api,IE8,Post Complex Data

时间:2013-09-24 18:17:37

标签: jquery asp.net-mvc internet-explorer-8 asp.net-web-api cors

作为我工作环境的一部分,我们需要支持IE8,但我们希望继续推进技术,特别是CORS。

我无法在ie8中将复杂对象发布到cors服务。该对象为null。以下是重现的步骤。如果需要,我可以将项目上传到github。

我创建了一个新的mvc4项目。添加了API控制器。并做了以下更改。

支持预检复杂的cors调用(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(); }
    }

来源:Handling CORS Preflight requests to ASP.NET MVC actions

支持text / plain(ie8只发送带有cors的text / plain)(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); 

        ...
    }

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

支持除动词以外的其他功能名称(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 }
        );

        ...
    }

支持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控制器,我调用了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;
    }

这是服务变更的程度。接下来我创建一个客户端。这也是一个mvc4项目。这里非常基本的东西。

使用cors(index.cshtml)对polyfill ie8进行polyfill:

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

来源:https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest

致电科尔服务

 $(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));
        });
    });

如前所述,所有3个电话都在ie8完成。但是服务中的请求对象在ie8中为null,在firefox中它被填充,即使我强制内容类型为text / plain

IE8控制台输出:

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

Firefox控制台输出:

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

2013年9月25日更新

我可以确认正在发送正文,但是没有被web api解析。如果我添加以下hack,它将按预期返回数据。在Firefox中,正文将为空,并填充请求对象。在ie8中,主体仍包含内容,请求为空。

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

2 个答案:

答案 0 :(得分:3)

这是我正在谈论的代码。创建这个作为一个新类,我在我的WebAPI项目中创建了一个DelegatingHandlers文件夹(但是,我还有一个过滤器文件夹,一个模型绑定文件夹...)

我收录了很多您可以轻松删除的评论。

以下假设IE 8/9将始终发送“JSON”数据。如果您的webAPI实现允许内容协商,并且您想要为IE8 / 9包含该功能,那么您显然需要在下面的代码中添加一些if语句,但这应该足以让您前进。我个人刚刚声明我只接受来自IE 8/9的JSON。

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

我的WebAPIConfig文件如下所示:

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

然后为了确保我的POST调用符合IE 8和9,在我的JS中我放了以下内容(但显然你只需要包括这个,如果你也在使用你自己的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.';
    }
},

就个人而言,我正在使用JSONP进行GET,而不是使用PUTS或DELETES,这对我来说已经足够了。如果我再次做这个项目,我会使用PUTS和DELETES。要使IE 8/9处理跨域PUTS并删除其显然常见的做法,在发送的数据或标题中包含一个新节点,称为“Type”的某个变体,并使用字符串“PUT”或“DELETE” ”。我不知道在哪里我会嗅到它。

启用CORS就像将以下内容放在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>

正如您在上面的评论中所看到的,您还可以通过发起网址(*)和请求类型(put,post等)来限制CORS。完全不需要stuff like this 完全This guy's blog gives a really good walkthrough.

这就是你需要对一个全新的WebAPI项目做的所有事情,使它支持CORS和IE 8/9。

答案 1 :(得分:0)

直到我能找到另一个解决方案或者我们可以停止支持IE8这里是接受的黑客攻击。感谢同事想出这个。

  1. 删除对global.asax中text / plain的支持,ie8发送的标头全为空。正如评论中所讨论的,请求正文不会自动解析。内容保留在体内。通常(比如在Firefox中)将正文解析为请求对象并替换为空字符串。
  2. 在App_Start中创建一个名为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. 如下更改人员控制器

    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;
    }
    
  4. IE8现在能够发送复杂的数据。