Cómo admitir el verbo HTTP OPTIONS en la aplicación ASP.NET MVC / WebAPI

80

He configurado una aplicación web ASP.NET comenzando con una plantilla MVC 4 / Web API. Parece que las cosas están funcionando muy bien, no hay problemas que yo sepa. He utilizado Chrome y Firefox para recorrer el sitio. Probé con Fiddler y todas las respuestas parecen estar relacionadas con el dinero.

Así que ahora procedo a escribir un Test.aspx simple para consumir esta nueva API web. Las partes relevantes del guión:

<script type="text/javascript">
    $(function () {

        $.ajax({
            url: "http://mywebapidomain.com/api/user",
            type: "GET",
            contentType: "json",
            success: function (data) {

                $.each(data, function (index, item) {

                    ....

                    });
                }
                );

            },
            failure: function (result) {
                alert(result.d);
            },

            error: function (XMLHttpRequest, textStatus, errorThrown) {
                alert("An error occurred, please try again. " + textStatus);
            }

        });

    });
</script>

Esto genera un encabezado REQUEST:

OPTIONS http://host.mywebapidomain.com/api/user HTTP/1.1
Host: host.mywebapidomain.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://mywebapidomain.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Connection: keep-alive

Tal como está, Web API devuelve un método 405 no permitido.

HTTP/1.1 405 Method Not Allowed
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Mon, 30 Sep 2013 13:28:12 GMT
Content-Length: 96

<Error><Message>The requested resource does not support http method 'OPTIONS'.</Message></Error>

Entiendo que el verbo OPTIONS no está conectado en los controladores Web API de forma predeterminada ... Entonces, coloqué el siguiente código en mi UserController.cs:

// OPTIONS http-verb handler
public HttpResponseMessage OptionsUser()
{
    var response = new HttpResponseMessage();
    response.StatusCode = HttpStatusCode.OK;
    return response;
}

... y esto eliminó el error 405 Método no permitido, pero la respuesta está completamente vacía, no se devuelven datos:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Mon, 30 Sep 2013 12:56:21 GMT
Content-Length: 0

Debe haber lógica adicional ... No sé cómo codificar correctamente el método de Opciones o si el controlador es el lugar adecuado para poner el código. Es extraño (para mí) que el sitio de la API web responda correctamente cuando se ve desde Firefox o Chrome, sin embargo, la llamada .ajax menciona los errores anteriores. ¿Cómo manejo la verificación "previa al vuelo" en el código .ajax? ¿Quizás debería abordar este problema en la lógica .ajax del lado del cliente? O, si esto es un problema en el lado del servidor debido a no manejar el verbo OPTIONS.

¿Alguien puede ayudar? Este debe ser un problema muy común y me disculpo si se ha respondido aquí. Busqué pero no encontré ninguna respuesta que me ayudara.

ACTUALIZAR En mi humilde opinión, este es un problema del lado del cliente y tiene que ver con el código Ajax JQuery anterior. Digo esto porque Fiddler no muestra ningún encabezado de error 405 cuando accedo a mywebapidomain / api / user desde un navegador web. El único lugar donde puedo duplicar este problema es desde la llamada JQuery .ajax (). Además, la llamada Ajax idéntica anterior funciona bien cuando se ejecuta en el servidor (mismo dominio).

Encontré otra publicación: La solicitud de prototipo AJAX se envía como OPCIONES en lugar de GET; da como resultado un error 501 que parece estar relacionado, pero he modificado sus sugerencias sin éxito. Aparentemente, JQuery está codificado de modo que si una solicitud Ajax es de dominio cruzado (que es la mía), agrega un par de encabezados que activan el encabezado OPTIONS de alguna manera.

'X-Requested-With': 'XMLHttpRequest',
'X-Prototype-Version': Prototype.Version,

Simplemente parece que debería haber una mejor solución disponible que modificar el código central en JQuery ...

La respuesta proporcionada a continuación asume que esto es un problema del lado del servidor. Quizás, supongo, pero me inclino por el cliente y llamar a un proveedor de alojamiento no va a ayudar.

rwkiii
fuente
¿Qué vas a enviar con tu solicitud de opciones?
Daniel A. White
No necesito enviar una solicitud de OPCIONES en absoluto. Por alguna razón, esto se hace cuando una llamada Ajax se realiza entre dominios. Entonces, como puede ver en Javascript, todo lo que estoy haciendo es especificar GET, pero el encabezado OPTIONS se envía debido al protocolo HTTP. Es una verificación "previa al vuelo".
rwkiii
2
oh, deberías habilitar cors en tu servidor iis.
Daniel A. White
Es un servidor Arvixe - Business Class Pro. Ambos sitios están alojados en el mismo servidor físico, la misma cuenta de alojamiento. Solo diferentes nombres de host. ¿Es CORS algo que puedo habilitar sin llamar a Arvixe?
rwkiii
Llamaría a su proveedor de alojamiento.
Daniel A. White

Respuestas:

52

Como dijo Daniel A. White en su comentario, lo más probable es que el cliente cree la solicitud OPTIONS como parte de una solicitud JavaScript entre dominios. Esto lo hacen automáticamente los navegadores compatibles con Cross Origin Resource Sharing (CORS). La solicitud es una solicitud preliminar o previa al vuelo , realizada antes de la solicitud AJAX real para determinar qué verbos y encabezados de solicitud son compatibles con CORS. El servidor puede optar por admitirlo para ninguno, todos o algunos de los verbos HTTP.

Para completar la imagen, la solicitud AJAX tiene un encabezado "Origen" adicional, que identifica desde dónde se entregó la página original que aloja el JavaScript. El servidor puede optar por admitir solicitudes de cualquier origen, o solo para un conjunto de orígenes conocidos y confiables. Permitir cualquier origen es un riesgo de seguridad, ya que puede aumentar el riesgo de falsificación de solicitudes entre sitios (CSRF).

Por lo tanto, debe habilitar CORS.

Aquí hay un enlace que explica cómo hacer esto en ASP.Net Web API

http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api#enable-cors

La implementación que se describe allí le permite especificar, entre otras cosas

  • Soporte CORS por acción, por controlador o globalmente
  • Los orígenes apoyados
  • Al habilitar el controlador CORS aa o el nivel global, los verbos HTTP admitidos
  • Si el servidor admite el envío de credenciales con solicitudes de origen cruzado

En general, esto funciona bien, pero debe asegurarse de conocer los riesgos de seguridad, especialmente si permite solicitudes de origen cruzado desde cualquier dominio. Piense con mucho cuidado antes de permitir esto.

En términos de qué navegadores admiten CORS, Wikipedia dice que los siguientes motores lo admiten:

  • Gecko 1.9.1 (FireFox 3.5)
  • WebKit (Safari 4, Chrome 3)
  • MSHTML / Trident 6 (IE10) con soporte parcial en IE8 y 9
  • Presto (Opera 12)

http://en.wikipedia.org/wiki/Cross-origin_resource_sharing#Browser_support

Mike Goodwin
fuente
Hola Mike. Gracias por el enlace, aquí hay otro que también es bueno: codeguru.com/csharp/.net/net_asp/… - aunque ninguno de estos me ha solucionado el problema, todavía. He colocado mis páginas de prueba en el servidor por ahora y eso me ayuda a corto plazo. Intenté instalar Microsoft.AspNet.WebApi.Cors pero recibí un error extraño de que mi aplicación no tenía ninguna dependencia de WebApi y, por lo tanto, la instalación se revertió. Gracias por tu respuesta, sé que es correcta. +1!
rwkiii
@rwkiii El enlace es de hecho una solución que implica agregar dependencias en Web API 5.2.2, pero la solución sería más extensible que un truco para obligar a MVC a admitir la solicitud previa al vuelo OPTIONS. Es posible que también desee revisar la respuesta de Dominick, ya que la solicitud previa al vuelo puede ser el resultado de los encabezados Aceptar O Tipo de contenido que requieren dicha llamada del cliente
Sudhanshu Mishra
Solo una nota, pero si establece el tipo de contenido en: 'application / x-www-form-urlencoded', 'multipart / form-data' o 'text / plain', la solicitud se considera 'simple' y no emitirá el solicitud previa al vuelo.
mbx-mbx
94

La respuesta de Mike Goodwin es excelente, pero parecía, cuando la probé, que estaba dirigida a MVC5 / WebApi 2.1. Las dependencias para Microsoft.AspNet.WebApi.Cors no funcionaron bien con mi proyecto MVC4.

La forma más sencilla de habilitar CORS en WebApi con MVC4 fue la siguiente.

Tenga en cuenta que he permitido todos, le sugiero que limite el Origen solo a los clientes a los que desea que atienda su API. Permitir todo es un riesgo de seguridad.

Web.config:

<system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Methods" value="GET, PUT, POST, DELETE, HEAD" />
        <add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, Content-Type, Accept" />
      </customHeaders>
    </httpProtocol>
</system.webServer>

BaseApiController.cs:

Hacemos esto para permitir las OPCIONES http verbo

 public class BaseApiController : ApiController
  {
    public HttpResponseMessage Options()
    {
      return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
    }
  }
Oliver
fuente
@Castaldi, esto probablemente se deba a que la respuesta proporcionada estaba dirigida a WebApi 1, que no tenía enrutamiento de atributos. Para WebApi 2, sugeriría usar el paquete nuget CORS de Microsoft. nuget.org/packages/Microsoft.AspNet.WebApi.Cors
Oliver
También puede utilizar [ApiExplorerSettings (IgnoreApi = true)] para ignorar los puntos finales OPTIONS en Swagger.
Mário Meyrelles
Esto funcionó para mi aplicación WebApi 2. Especialmente el método Options () para el controlador base / relevante
lazyList
24

Simplemente agregue esto a su Application_OnBeginRequestmétodo (esto habilitará el soporte CORS a nivel mundial para su aplicación) y "manejar" las solicitudes de verificación previa:

var res = HttpContext.Current.Response;
var req = HttpContext.Current.Request;
res.AppendHeader("Access-Control-Allow-Origin", req.Headers["Origin"]);
res.AppendHeader("Access-Control-Allow-Credentials", "true");
res.AppendHeader("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version, X-File-Name");
res.AppendHeader("Access-Control-Allow-Methods", "POST,GET,PUT,PATCH,DELETE,OPTIONS");

// ==== Respond to the OPTIONS verb =====
if (req.HttpMethod == "OPTIONS")
{
    res.StatusCode = 200;
    res.End();
}

* seguridad: tenga en cuenta que esto habilitará las solicitudes ajax desde cualquier lugar a su servidor (en su lugar, solo puede permitir una lista separada por comas de Orígenes / URL si lo prefiere).

Usé el origen del cliente actual en lugar de *porque esto permitirá credenciales => la configuración Access-Control-Allow-Credentialsen verdadero habilitará la administración de sesiones de navegador cruzado

también debe habilitar los verbos de eliminación y colocación, parche y opciones en su webconfigsección system.webServer, de lo contrario, IIS los bloqueará:

<handlers>
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
  <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

espero que esto ayude

Chtiwi Malek
fuente
3
Gracias. Solo esto Application_OnBeginRequestme ayudó. Pero si también desea poder obtener datos con autorización, también debe agregar AuthorizationaAccess-Control-Allow-Headers
Ehsan88
18

Después de encontrar el mismo problema en un proyecto Web API 2 (y no poder usar los paquetes CORS estándar por razones que no vale la pena mencionar aquí), pude resolverlo implementando un DelagatingHandler personalizado:

public class AllowOptionsHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);

        if (request.Method == HttpMethod.Options &&
            response.StatusCode == HttpStatusCode.MethodNotAllowed)
        {
            response = new HttpResponseMessage(HttpStatusCode.OK);
        }

        return response;
    }
}

Para la configuración de la API web:

config.MessageHandlers.Add(new AllowOptionsHandler());

Tenga en cuenta que también tengo los encabezados CORS habilitados en Web.config, similar a algunas de las otras respuestas publicadas aquí:

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true">
    <remove name="WebDAVModule" />
  </modules>

  <httpProtocol>
    <customHeaders>
      <add name="Access-Control-Allow-Origin" value="*" />
      <add name="Access-Control-Allow-Headers" value="accept, cache-control, content-type, authorization" />
      <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
    </customHeaders>
  </httpProtocol>

  <handlers>
    <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
    <remove name="TRACEVerbHandler" />
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
  </handlers>
</system.webServer>

Tenga en cuenta que mi proyecto no incluye MVC, solo Web API 2.

define
fuente
10

He logrado superar los errores 405 y 404 lanzados en las solicitudes de opciones de Ajax previas al vuelo solo mediante código personalizado en global.asax

protected void Application_BeginRequest()
    {            
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
        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", "GET, OPTIONS");
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
            HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
            HttpContext.Current.Response.End();
        }
    }

PD: Tenga en cuenta los problemas de seguridad al permitir todo *.

Tuve que deshabilitar CORS ya que estaba devolviendo el encabezado 'Access-Control-Allow-Origin' que contiene varios valores.

También necesitaba esto en web.config:

<handlers>
  <remove name="ExtensionlessUrlHandler-Integrated-4.0"/>
  <remove name="OPTIONSVerbHandler"/>
  <remove name="TRACEVerbHandler"/>
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
</handlers>

Y app.pool debe configurarse en modo integrado.

lukalev
fuente
8

Yo tuve el mísmo problema. Para mí, la solución fue eliminar el tipo de contenido personalizado de la llamada jQuery AJAX. Los tipos de contenido personalizados activan la solicitud previa al vuelo. Encontré esto:

El navegador puede omitir la solicitud de verificación previa si se cumplen las siguientes condiciones:

El método de la petición es GET, HEADo POST, y

La aplicación no establece ningún cabeceras de petición distinta Accept, Accept-Language, Content-Language, Content-Type, o Last-Event-ID, y

El Content-Typeencabezado (si está configurado) es uno de los siguientes:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Desde esta página: http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api (en "Solicitudes de verificación previa")

Dominik
fuente
2
    protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 405 && Context.Request.HttpMethod == "OPTIONS" )
        {
            Response.Clear();
            Response.StatusCode = 200;
            Response.End();
        }
    }
yongfa365
fuente
1

Yo también enfrenté el mismo problema.

Siga el paso a continuación para resolver el problema de conformidad (CORS) en los navegadores.

Incluya REDRock en su solución con la referencia de Cors. Incluya la referencia de WebActivatorEx a la solución Web API.

Luego, agregue el archivo CorsConfig en la carpeta App_Start de la API web.

[assembly: PreApplicationStartMethod(typeof(WebApiNamespace.CorsConfig), "PreStart")]

namespace WebApiNamespace
{
    public static class CorsConfig
    {
        public static void PreStart()
        {
            GlobalConfiguration.Configuration.MessageHandlers.Add(new RedRocket.WebApi.Cors.CorsHandler());
        }
    }
}

Con estos cambios realizados, pude acceder a webapi en todos los navegadores.

Praveen Thangaraja
fuente
4
¿Qué es Redrock? Hice una búsqueda en Google y una búsqueda de paquetes Nuget y no devolvió nada. Un enlace estaría bien.
hofnarwillie
1

Tuve el mismo problema, y ​​así es como lo solucioné:

Solo lanza esto en tu web.config:

<system.webServer>
    <modules>
      <remove name="WebDAVModule" />
    </modules>

    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Expose-Headers " value="WWW-Authenticate"/>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS, PUT, PATCH, DELETE" />
        <add name="Access-Control-Allow-Headers" value="accept, authorization, Content-Type" />
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>

    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
</system.webServer>
Erti-Chris Eelmaa
fuente
0
//In the Application_OnBeginRequest method in GLOBAL.ASX add the following:-  

var res = HttpContext.Current.Response;  
var req = HttpContext.Current.Request;  
res.AppendHeader("Access-Control-Allow-Origin", "*");  
res.AppendHeader("Access-Control-Allow-Credentials", "true");  
res.AppendHeader("Access-Control-Allow-Headers", "Authorization");  
res.AppendHeader("Access-Control-Allow-Methods", "POST,GET,PUT,PATCH,DELETE,OPTIONS");  

    // ==== Respond to the OPTIONS verb =====
    if (req.HttpMethod == "OPTIONS")
    {
        res.StatusCode = 200;
        res.End();
    }

//Remove any entries in the custom headers as this will throw an error that there's to  
//many values in the header.  

<httpProtocol>
    <customHeaders>
    </customHeaders>
</httpProtocol>
Roy Fagon
fuente