昨天,Twitter上一个标记为#psr7
的问题引起了我的注意:
#psr7
Request::getHeader($name)
返回单个字符串数组而不是#Slim3中的字符串?抄送:@codeguypic.twitter.com/ifA9hCKAPs@feryardiant(推文)
#psr7
Request::getHeader($name)
返回单个字符串数组而不是#Slim3中的字符串?抄送:@codeguypic.twitter.com/ifA9hCKAPs@feryardiant(推文)
链接的图片提供了以下详细信息:
例如,当我调用
$request->getHeader('Accept')
时,我预计我会得到这样的结果:Array( [0] => text/html, [1] => application/xhtml+xml, [2] => application/xml, )但是,实际上我得到了这个:
Array( [0] => text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 )它是正确的吗?
在这篇文章中,我将解释为什么观察到的行为是正确的,并阐明PSR-7中标头处理的一些细节。
PSR-7中的标头
在创建PSR-7规范时,我们不得不兼顾各种HTTP规范的大量细节。由于规范的灵活性和模糊性,标头是一个特别困难的领域。
歧义的根源在于标头允许具有多个值。标头可能具有多个值,但这取决于任何给定标头的规范。
另外,如何表示多个值取决于给定的标题。HTTP规范允许对同一标头使用多个调用:
X-Foo-Bar: baz X-Foo-Bar: bat
以上意味着X-Foo-Bar
标头有两个值,baz
和bat
。假设标头完全允许多个值;如果不是,则它只有一个值,最后一个表示获胜(bat
,如果你注意的话)。
表示多个值的另一种方法是使用分隔符。规范表明,如果您希望在同一标题行中有多个值,您应该
使用逗号(,
)作为分隔符。但是,您可以
使用您想要的任何其他分隔符。SetCookie
标头是允许使用完全不同的分隔符(分号)的多个值的标头的主要示例!
总结一下:
- 标头可能允许也可能不允许多个值。
- 标头可能会发出多次。如果一个标头允许多个值,那么它的值是每个表示的集合。如果标题只允许一个值,则最后表示是该标题的规范值。
- 标题可以在一行中使用分隔符以分隔多个值。该字符建议为逗号,但它可以因标头而异。
规范中的另一个大歧义是规范可扩展,并且特别允许自定义标头。
这意味着任何表示HTTP的通用代码,例如PSR-7,都不可能知道管理所有可能的HTTP消息的整个规则集,因为它不知道所有潜在的标头类型,包括它们是否允许多个值。
考虑到这两个事实——标头可能有多个值,并且允许自定义标头——我们对PSR-7做出了以下决定:
所有标题都是集合
假定所有标头都具有多个值。这提供了使用的一致性,并将了解任何给定标头的语义的责任交给了消费者。
因此,对给定标头的最基本访问,getHeader($name)
,返回一个数组。该数组可以具有以下值:
- 可以为空;这意味着标头没有或不会出现在表示中。
- 单个字符串值。
- 多个字符串值。
朴素连接
由于大多数标头只允许单个值,并且由于大多数现有的解析标头的库只接受字符串,我们提供了另一种方法,getHeaderLine($name)
。此方法保证返回一个字符串:
- 如果标头没有值,则字符串将为空。
- 否则,它将使用逗号连接值。
我们选择not来提供一个参数来指示要使用的分隔符,因为该规范仅指示逗号作为分隔符,同时也是为了降低实现的复杂性。如果你想使用不同的分隔符,你可以自己使用implode($separator,$message->getHeader($name))
。
无解析
因为每个标头的分隔符不同,并且因为不同的标头在如何解释数据方面有不同的规范,并且因为规范允许我们无法在通用库中编码的自定义标头,所以我们决定PSR-7实现不得解析提供给他们的标头值。
实际上这有两个作用:
- 对于传入的请求,即使标头允许多个逗号分隔值,实现也必须保持它们不变。这确保不会丢失数据。
- 对于复杂值,您必须将它们传递给解析器以分解和解释它们。
该规则还有另一个动机:为具有多个值的emittingheaders提供语义,作为单行或多行。如果所有值连接在一行中,客户端或服务器可以假设消息应该作为单行发送或接收,而多行数组将指示多个标题行。这允许消费者来决定标题应该如何表示!
后果
我们选择的路径产生了一些有趣的结果。首先,我们最终得到了一个高度一致的API。当我调用getHeader()
或getHeaderLine()
时,我可以预期哪些数据类型是明确的。其次,我可以放心,一旦我获得其中一个操作的结果,就不会丢失数据;没有进程试图解析值和潜在的改变。
反面是之前的Twitter评论。让我们再看一遍。
分解
让我们回顾一下作者从getHeader('Accept')
调用中收到的内容:
Array( [0] => text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 )
Accept
标头允许多个值,但期望它们是单个逗号连接的字符串。与作者的预期相反,上面代表了以下值:
[ 'text/html', 'application/xhtml+xml', 'application/xml;q=0.9', 'image/web', '*/*;q=0.8', ]
请注意,这些值包括内联的;q=*
符号!Accept
标头用逗号分隔值,但每个值也可以有额外的键/值属性,也用分号分隔。
为什么上面的不是我们从getHeader()
得到的?它可以追溯到我提到的关于PSR-7标头处理的最后一条规则:不解析。Acceptheader规范表明多个值应该在同一行,用逗号分隔,这正是浏览器将它发送到服务器的方式;PSR-7将行按原样设置为数组中的唯一值。
建议
上面的例子提供了另一个很好的教训:复杂的值应该有专门的解析器。PSR-7字面上只处理HTTP消息的低级细节,不提供对它的解释。一些标头值,例如Accept
标头,需要专门的解析器来理解该值。
这个值表示什么?
- 客户端尽可能使用
text/html
、application/xhtml+xml
和image/webp
表示;如果这三个中的任何一个可用,则按此顺序首选它们 - 如果以上都不可用,则首选的下一个表示是
application/xml
. - 否则可能会退回任何其他陈述。
我怎么知道的?通过阅读Accept标头规范。这非常复杂。并且已经为此编写了许多库,可以接受Accept标头值、解析它并为您返回优先队列。PSR-7充当此类库的数据源,但自身不进行解析。
鳍
希望这篇文章揭开了PSR-7如何表示和处理HTTP标头的神秘面纱。PSR-7旨在反映HTTP规范的可扩展性,提供使用的一致性和数据完整性。
我们在元文档中提出的一个具体建议是将标头的任何处理委托给专门的库。当我们看到PSR-7的采用率上升时,我希望看到更多这样的东西出现。