HTTP_ACCEPT 是 'application/json, */*' 时 Rails 按 'text/html' 处理

用 Rails3 做移动端 App 的 Backend 时,出现一种情况,当移动端使用中国移动的网络时(中国联通无此状况),发送到服务端的 HTTP 请求的 Header 中的 Accept 莫名其妙的的被加上了 */*。而本来客户端只给 Accept 赋值了 application/json,现在就变成了 application/json, */*。Rails 在接收到这种请求时,request 的 format 就会识别为 text/html,导致 controller 最终返回给客户端的是 html 格式的应答。

为什么会被添加 */* 就不做深究了,网络运营商对数据的传输做了哪些处理策略不得而知,就算知道了也无能为力。于是考虑从服务端着手想办法解决。

之前为了发现为什么 Rails 返回的是 html 格式的应答时添加了一些 debug 日志,来观察客户端有没有正确的设置 HTTP Header。

app/controllers/application_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :log_request
def log_request
logger.debug "HTTP_ACCEPT = #{request.headers['HTTP_ACCEPT']}"
logger.debug "CONTENT_TYPE = #{request.headers['CONTENT_TYPE']}"
logger.debug "request.content_type = #{request.headers['action_dispatch.request.content_type']}"
logger.debug "request.accepts = #{request.headers['action_dispatch.request.accepts']}"
logger.debug "request.formats = #{request.headers['action_dispatch.request.formats']}"
end
end

正是根据这些日志发现了被添加的 */*,而且发现只有这一种情况会导致返回 html 应答,所以就针对这种情况做一下处理。
application_controller.rb 中添加如下代码:

1
2
3
4
5
6
7
8
9
before_filter :modify_request_header
def modify_request_header
if request.headers['HTTP_ACCEPT'].include? "*/*"
preferred_accept = request.headers['HTTP_ACCEPT'].split(",").first
logger.debug "Preferred ACCEPT: #{preferred_accept}"
request.format = :json if preferred_accept == "application/json"
end
end

采用这种方式修改了 request.formats 的值,于是终于可以正确返回 json 格式的应答了。

可是为什么 Rails 不按照 Accept 的值的顺序优先采用 application/json 指定的格式呢?

参考了这里

看来有些浏览器并不会按照正确的顺序设置 Accept 的值(比如IE7),所以当含有 */* 时,如果是一个非 XHR 请求(如 Ajax 请求),则 Rails 会采用 text/html,而如果收到的是一个 XHR 请求,则会按照 Accept 的值来处理。所以如果想要 Rails 依据 Accept 的值来处理格式,可以在 HTTP Header 添加 X_REQUESTED_WITH: XMLHttpRequest 参数。不过我尝试了这个方法,在我本地开发环境是OK的,但是当发送到远程服务器上时问题还是依旧,是什么原因我暂时就先不分析了,我只想先尽快解决这个问题,所以就用了上面的代码做了点处理。