Rails中的MIME类型解析规则
本文缘于在项目中遇到的一个问题,查阅了网上的资料和Rails源码后有一点收获,简单做个总结,有些地方不够全面,欢迎大家补充指正。
相关背景
Rails项目中经常可以看到类似如下代码:
|
|
如果想获取xml格式的数据,就在请求路径后面增加.xml
扩展名,比如localhost:3000/users.xml
,这样就可以拿到xml格式的返回。
路径扩展名是Rails中MIME类型解析的一个影响因素,另一个影响因素是HTTP头字段Accept。
HTTP头字段Accept
当浏览器发送请求的时候,它也会通知服务器自己能处理的内容类型。访问网站的时候可以通过浏览器的开发者工具查看它们发送的Accept头。
下面是我的机器上不同浏览器发送的Accept头:
Chrome: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Firefox: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Safari: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
让我们看看Chrome的Accept头:
Chrome: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Chrome的头表明它可以处理html文档(text/html)、xhtml文档(application/xhtml+xml)、xml文档(application/xml)、webp和apng格式的图片,以及其它别的格式。
Accept头里面有一个q值,它表示优先级。HTTP标准里面是这么描述的(https://tools.ietf.org/html/rfc7231#section-5.3.1):
5.3.1. Quality Values
Many of the request header fields for proactive negotiation use a common parameter, named “q” (case-insensitive), to assign a relative “weight” to the preference for that associated kind of content. This weight is referred to as a “quality value” (or “qvalue”) because the same parameter name is often used within server configurations to assign a weight to the relative quality of the various representations that can be selected for a resource.
The weight is normalized to a real number in the range 0 through 1, where 0.001 is the least preferred and 1 is the most preferred; a value of 0 means “not acceptable”. If no “q” parameter is present, the default weight is 1.
简单说就是为了表示Accept中内容类型的优先级,使用一个参数q来作为权重,q是一个0到1之间的实数,最小是0.001,最大是1。如果是0表示不接受这种内容类型。如果没有指定,q默认值为1。
Accept内容类型的优先级除了与q值和顺序有关,还与类型具体化程度有关,越具体的类型优先级越高(https://tools.ietf.org/html/rfc7231#section-5.3.2):
Media ranges can be overridden by more specific media ranges or specific media types. If more than one media range applies to a given type, the most specific reference has precedence.
举RFC里的例子:
Accept: text/*, text/plain, text/plain;format=flowed, */*
这个Accept头里,类型优先级如下:
- text/plain;format=flowed
- text/plain
- text/*
- /
关于Accept优先级到此为止,不继续深究,有兴趣的同学可以阅读RFC原文。
遇到的问题
我们的项目采用前后端分离的写法,后端controller中有如下代码:
|
|
前端发送的请求头中Accept字段如下:
Accept: application/json, text/plain, */*
按我的理解,接口应该返回json数据,结果返回的是html重定向。 经过后来几次测试、跟踪源码,现象如下:
Accept | 返回 |
---|---|
application/json, text/plain, */* |
html重定向 |
application/json, text/plain |
json数据 |
*/* |
与respond_to代码块中声明格式的顺序有关,即format.html 、format.json 哪个在前,返回哪个 |
除了第一条,后面两条基本还是符合直觉的。接下来看看Rails判断返回格式的规则到底是什么样的?
Rails对MIME的解析
答案当然要从Rails源码中找(Read the fucking source code ^_^),涉及的函数如下(在文件rails/actionpack/lib/action_dispatch/http/mime_negotiation.rb
中,代码细节先不用深究,后文还有分析):
|
|
|
|
|
|
|
|
通过以上代码,可以大体看出Rails对返回格式的判断流程:
- 判断请求是否带format后缀,如果带,则返回相应格式。如:http://localhost:3000/users.xml。
- 判断请求是否有Accept头并且是合法头,如果是,则解析Accept,返回Accept中的格式。
- 判断请求是否有
format_from_path_extension
的格式,不好意思,这个暂时没来得及研究是啥,欢迎大神们补充^_^。 - 判断请求是否是Ajax调用,如果是,返回javascript格式数据。
- 以上都不满足,返回html格式。
对浏览器的特殊处理
从上面的代码里看到,Rails对浏览器发出的请求做了特殊处理,如果是浏览器发出的,则不解析Accept头。为什么要对浏览器的请求做特殊处理?查阅的资料显示是因为早期浏览器设计不规范,大部分浏览器的请求头Accept字段第一个值是application/xml
,如果按照Accept解析就会给浏览器用户返回xml格式的数据,而这通常不是浏览器用户想要的。因此判断如果是浏览器就直接忽略Accept头。
最后总结一下Accept的三种情形。
Accept三种情形
1. Accept不包含*/*
假设接口中有如下代码:
|
|
Accept的值为:
application/json, text/plain
此时formats
函数中valid_accept_header
返回true,Rails会解析Accept,formats
函数最终返回如下格式顺序的数组:
json
plain
negotiate_mime
函数中的order
数组中包含的格式是:
html
json
这种情况下,代码遍历formats
,如果order
中有匹配的格式就返回。format
的第一个格式是json,order
中有匹配,此时返回json数据。
结论:Accept不包含*/*时,按照Accept的优先级返回。
2. Accept头是*/*
Accept的值为:
*/*
接口代码如下:
|
|
此时返回html格式。
把respond_to
代码块调整一下顺序:
respond_to do |format|
format.json { render json: { data: 'this is json' } }
format.html { render html: '<p>this is html</p>'.html_safe }
end
此时返回json格式。
Accept头是*/*时,解析Accept的结果为Mime::ALL
类型,从negotiate_mime
函数代码中可以明显看出,当请求类型为Mime::ALL
时,选择order
中的第一个格式返回,此时会返回respond_to
代码块中的第一个格式。
如果没有respond_to
代码块呢?如果没有respond_to
代码块,但是在view目录下有test.html.erb
、test.json.jbuilder
两个文件,Rails返回什么格式?这种情况下,Rails按顺序遍历所有注册的Mime格式,找对应的匹配文件,一旦找到文件就返回该格式。这种情况下的返回格式依赖Mime格式的注册顺序,下面是Mime格式注册的代码(rails/actionpack/lib/action_dispatch/http/mime_types.rb
):
|
|
很明显,text/html
是第一个格式,因此在本例中返回test.html.erb
文件内容。
结论:Accept头是*/*时,返回respond_to
代码块中的第一个格式。没有respond_to
代码块时,按Mime格式注册顺序寻找对应文件返回。
3. Accept头包含*/*和其他内容
Accept的值为:
application/json, text/plain, */*
接口代码如下:
respond_to do |format|
format.json { render json: { data: 'this is json' } }
format.html { render html: '<p>this is html</p>'.html_safe }
end
暂时不考虑流程中的3、4,此时返回html,不会解析Accept头(原因在于valid_accept_header
函数返回false,对浏览器的特殊处理),也不会受respond_to
代码块顺序影响。
结论:Accept头包含*/*和其他内容,返回html。
文笔不好,内容又多,写的有点乱,大家见谅。
参考资料:
https://blog.bigbinary.com/2010/11/23/mime-type-resolution-in-rails.html
https://github.com/rails/rails/issues/9940