原型 AJAX 请求被发送为 OPTIONS 而不是 GET,导致 501 错误。

8
我正在尝试使用Prototype/AJAX访问Web服务,但遇到一个无法解决的错误:当我向服务器发出请求时,我的请求被解释为OPTIONS而不是GET请求(因此根据我从Access-Control-Request-Method:中了解到的内容,会引发501-未实现的错误,因为服务器只允许GET请求)。 我在AJAX / request公式中是否缺少某些东西可能导致此错误? 我已经阅读了一些关于CORS /预检请求的信息在这里,但我不确定它如何适用于我的代码看起来是符合规范的...
以下是相关的AJAX请求:
function fetchMetar() {
var station_id = $("station_input").value;

    new Ajax.Request(REQUEST_ADDRESS, {
        method: "get",
        parameters: {stationString: station_id},
        onSuccess: displayMetar,
        onFailure: function() {
            $("errors").update("an error occurred");
        }
    });
}

这里是我从Chrome得到的错误和相关请求信息:

Request URL:http://weather.aero/dataserver_current/httpparam?
 dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3
 &mostRecent=true&stationString=&stationString=KSBA
Request Method:OPTIONS
Status Code:501 Not Implemented
Request Headers
Accept:*/*
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Access-Control-Request-Headers:origin, x-prototype-version, x-requested-with, accept
Access-Control-Request-Method:GET
Connection:keep-alive
Host:weather.aero
Origin:http://domain.com
Referer:http://domain.com/.../...html

我可能忽略了什么?为什么Chrome说请求是以OPTIONS而不是GET发送的?当Chrome输出Access-Control-Request-Headers:信息时,这些是否是请求中允许的唯一标头?谢谢!
4个回答

15
在原型js上花费了太多时间寻找正确的修复方法...最终,在伟大的Kourge(Wilson Lee)文章中,我们找到了一种非侵入式的解决方案!以下是摘录:
“大多数主要的Ajax框架都喜欢在你实例化的Ajax请求上设置自定义HTTP标头;最流行的标头是X-Requested-With: XMLHttpRequest。因此,您的请求被提升为预检请求并失败了。如果您的请求跨域,则必须防止JavaScript框架设置这些自定义标头。jQuery通过不设置自定义标头来巧妙地避免无意中进行预检请求,如果您的URL被认为是远程,则会考虑到这一点。如果您使用其他框架,则必须手动防止此操作。”
可以简单地这样做:
new Ajax.Request('http://www.external-domain.net/my_api.php?getParameterKey=getParameterValue', {
            method:'post',
            contentType:"application/x-www-form-urlencoded",
            postBody:'key=' + value,
            onSuccess: function(response) {
                // process response
            },
            onCreate: function(response) { // here comes the fix
                var t = response.transport; 
                t.setRequestHeader = t.setRequestHeader.wrap(function(original, k, v) { 
                    if (/^(accept|accept-language|content-language)$/i.test(k)) 
                        return original(k, v); 
                    if (/^content-type$/i.test(k) && 
                        /^(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)(;.+)?$/i.test(v)) 
                        return original(k, v); 
                    return; 
                }); 
            } 
        });

如果您对这个解决方案有任何不利之处或改进意见,欢迎分享 :)

经过数小时的搜索,我找到了这个答案,正是我所需要的 :) - Aditya Singh

6
事实上这是一个预检请求, 因为Prototype添加了自定义头部X-Requested-With, X-Prototype-Version到请求中,浏览器因此会发送第一个OPTIONS请求。XHR规范解释如下:

对于非同源请求,当设置了除Accept和Accept-Language之外的其他头时,使用HTTP GET方法会发出预检请求。

如何解决这个问题?我只能看到一种尽快解决的可能性:完全重写Ajax.Request#setRequestHeaders()方法,例如在Prototype.js之后插入这个脚本:
Ajax.Request.prototype.setRequestHeaders = function() {
  var headers = {
    // These two custom headers cause preflight request:
    //'X-Requested-With': 'XMLHttpRequest',
    //'X-Prototype-Version': Prototype.Version,
    'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
  };

  if (this.method == 'post') {
    headers['Content-Type'] = this.options.contentType +
      (this.options.encoding ? '; charset=' + this.options.encoding : '');

    /* Force "Connection: close" for older Mozilla browsers to work
     * around a bug where XMLHttpRequest sends an incorrect
     * Content-length header. See Mozilla Bugzilla #246651.
     */
    if (this.transport.overrideMimeType &&
        (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
          headers['Connection'] = 'close';
  }

  if (typeof this.options.requestHeaders == 'object') {
    var extras = this.options.requestHeaders;

    if (Object.isFunction(extras.push))
      for (var i = 0, length = extras.length; i < length; i += 2)
        headers[extras[i]] = extras[i+1];
    else
      $H(extras).each(function(pair) { headers[pair.key] = pair.value; });
  }

  for (var name in headers)
    this.transport.setRequestHeader(name, headers[name]);
}

这个补丁会从任何AJAX请求中删除自定义标头。如果您仍然需要这些标头进行非CORS请求,可以添加更多逻辑,以便在 new Ajax.Request()选项中禁用这些标头(我将跳过此变体以使答案更短)。

1

实际上,使用Prototype.js V1.7会更容易:

Ajax.Responders.register({
    onCreate:function(r){
        r.options.requestHeaders={
        'X-Prototype-Version':null,
        'X-Requested-With':null
        };
    }
});

如果值为null,Prototype.js会删除任何预定义的标题。


0

我从未使用过Prototype,也不确定它对我有多大用处。但我快速查看了文档,没有看到对方法和参数的支持。

所以尝试:

new Ajax.Request(REQUEST_ADDRESS+"?stationString="+station_id, {
    onSuccess: displayMetar,
    onFailure: function() {
        $("errors").update("an error occurred");
    }
});

另外,我刚刚注意到你示例中的stationString应该加引号,假设它不是一个变量。


啊,Prototype肯定支持请求的方法和参数 - 我不确定问题是否出在那里(虽然我过去使用过这个确切的语法进行请求,当然为了不同的参数/服务器稍作修改)。不过我会继续尝试调整语法... - Philip David
只是确认一下,REQUEST_ADDRESS是相对/绝对URL吗?也就是说没有"http://"。现代浏览器由于安全问题限制了跨域请求。在答案中有解释链接。简而言之,请确保您发出请求的REQUEST_ADDRESS与调用所来自的主机域名匹配。 - Adam
这确实是一个绝对URL——我正在尝试访问的Web服务位于请求发起的主机域之外。然而,我以前使用过其他Web服务(例如Last.fm API),跨域请求并没有引起问题……为什么会这样(这是某些特定服务器的原因吗)?如果我想向另一个服务器发出请求,如何避免同域请求的限制?根据您提供的链接(顺便说一下,谢谢),看起来需要进行头部操作… - Philip David
嗯...这有点牵强,但你可以尝试设置Access-Control-Allow-Origin头。我看不出它会有多大的区别,因为它来自同一个域...除此之外,我就没有更多的想法了伙计。 - Adam
它是服务器特定的 - 服务器必须设置一个允许跨域请求的头。为了解决这个问题,您可以编写代理脚本,将信息来回传递到您的AjaxRequest。它还将允许您操纵或缓存它(如果您想要的话)。维基百科文章CORS解释了一个简单的例子。 - Geek Num 88

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接