使用multipart / form-data时,为什么没有正确发送带有Unicode的POST名称?

时间:2013-12-15 05:43:58

标签: python http unicode python-requests multipartform-data

我想发送附带文件的POST请求,但有些字段名称中包含Unicode字符。但是服务器没有正确接收它们,如下所示:

>>> # normal, without unicode
>>> resp = requests.post('http://httpbin.org/post', data={'snowman': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form']
>>> resp
{u'snowman': u'hello'}
>>>
>>> # with unicode, see that the name has become 'null'
>>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form']
>>> resp
{u'null': u'hello'}
>>>
>>> # it works without the image
>>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}).json()['form']
>>> resp
{u'\u2603': u'hello'}

我如何解决这个问题?

4 个答案:

答案 0 :(得分:22)

来自wireshark评论,看起来python-requests做错了,但可能没有“正确答案”。

RFC 2388

  

最初使用非ASCII字符集的字段名称可以使用RFC 2047中描述的标准方法在“name”参数的值内编码。

RFC 2047反过来说

  

通常,“编码字”是一系列可打印的ASCII字符,以“=?”开头,以“?=”结尾,中间有两个“?”。它指定了一个字符集和一种编码方法,并且还包括根据该编码方法的规则编码为图形ASCII字符的原始文本。

继续描述“Q”和“B”编码方法。使用“Q”(quoted-printable)方法,名称为:

=?utf-8?q?=E2=98=83?=

但是,正如RFC 6266明确指出:

  

“编码字”绝不能用于MIME内容类型或内容处置字段的参数,也不能用于任何结构化字段正文,除非在“评论”或“短语”中。

所以我们不允许这样做。 (感谢@Lukasa这次捕获!)

RFC 2388也说

  

也可以提供原始本地文件名      “filename”参数中的任何一个“content-disposition:form-data”      标题,或者在多个文件的情况下,在“内容处理:      文件“子部分的标题。发送应用程序可以提供一个      文件名;如果发件人的操作系统的文件名不是      在US-ASCII中,文件名可能是近似的,或使用编码      RFC 2231的方法。

RFC 2231描述了一种看起来更像你所看到的方法。在其中,

  

星号(“*”)被重复用于提供语言和语言的指示符      存在字符集信息并且正在使用编码。一个      单引号(“'”)用于分隔字符集和语言      参数值开头的信息。百分号      (“%”)用作编码标志,与RFC 2047一致。

     

具体来说,参数名称末尾的星号用作      字符集和语言信息可能出现的指示符      参数值的开头。单引号用于      将字符集,语言和实际值信息分开      参数值字符串,百分号用于标记      以十六进制编码的八位字节。

也就是说,如果使用此方法(并且两端都支持),则名称应为:

name*=utf-8''%E2%98%83

幸运的是,RFC 5987将基于RFC 2231 的编码添加到HTTP标头中! (感谢@bobince这个发现)它说你可以(任何可能应该)包含RFC 2231风格的值一个简单的值:

  

标题字段规范需要定义是否有多个实例      允许使用具有相同parmname组件的参数,以及如何使用      他们应该被处理。该规范建议a      使用扩展语法的参数优先。这个会      允许生产者使用这两种格式而不破坏收件人      还不了解扩展语法。

     

示例:

     
    

foo:bar; title =“欧元汇率”;                    标题* = UTF-8 '' %E2%82%AC%20exchange%20rates

  
然而,在他们的例子中,他们“愚弄”了“遗留客户”的普通价值。对于表单字段名称,这不是一个真正的选项,因此似乎最好的方法可能包括 name=name*=版本,其中普通值是(正如@bobince描述的那样)“只是以与表单相同的编码发送引用的字节”,如:

Content-Disposition: form-data; name="☃"; name*=utf-8''%E2%98%83

另见:

最后,请参阅http://larry.masinter.net/1307multipart-form-data.pdf(还有https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909#c8),其中建议您坚持使用ASCII表单字段名称来避免此问题。

答案 1 :(得分:4)

  

字段值显示为form-data; Wireshark中的name * = utf-8''%5Cu2603

这里有两件事。

  1. 这不适合我,我得name*=utf-8''%E2%98%83%5Cu2603是我在非Unicode字符串中意外键入\u转义符所期望的,即如上所述写'\u2603'而不是'☃'

  2. 正如所讨论的那样,这是扩展Unicode标头的RFC 2231形式:

  3. RFC 2231格式以前在HTTP中无效(HTTP不是RFC 822系列中的邮件标准)。它现在已经被RFC 5987带到了HTTP,但因为这是最近几乎没有任何服务器端支持它。

    绝对urllib3不应该依赖它;它应该做什么浏览器做,只是发送引用的字节,与表单相同的编码。如果它必须使用2231表格,它应该是组合的,如section 4.2。 例如在urllib3.fields.format_header_param中,而不是:

    value = email.utils.encode_rfc2231(value, 'utf-8')
    

    你可以说:

    value = '%s="%s"; %s*=%s' % (
        name, value, name,
        email.utils.encode_rfc2231(value, 'utf-8')
    )
    

    但是,包括2231表单可能仍然会混淆一些旧服务器。

答案 2 :(得分:1)

我想我应该责怪urllib3因此Requests产生它的格式。在I wrote that code时,我主要考虑了文件名,RFC 2388 section 4.5建议在那里使用RFC 2231格式。

关于字段名称,RFC 2388 section 3引用RFC 2047forbids依次and others使用Content-Disposition字段中的编码字。所以在我看来RFC 2338 should take precedence这两个标准相互矛盾。但也许made aware,所以使用RFC 2047编码的单词可能会更正确。

最近我the current draft for the HTML 5 standard已经a section on the encoding of multipart/form-data an issue for urllib3 {{3}}。它与其他几个标准相矛盾,但它可能是未来。关于字段名称(不是文件名),它描述了一种编码,该编码将字符转换为十进制XML实体,例如, ☃雪人。但是,只有在为提交而建立的编码不包含相关字符时才应用该编码,而在您的设置中不应该这样。

我已提交{{3}}来讨论此问题的后果,并可能在实施过程中解决这些问题。

答案 3 :(得分:0)

Rob Starling的回答非常有见地,并且证明在字段名称中使用非ASCII字符是兼容性方面的错误(所有那些RFC!),但我设法让python请求坚持使用最多(来自我所能看到的)处理事物的方法。

site-packages/requests/packages/urllib3/fields.py内,删除此(第50行):

value = email.utils.encode_rfc2231(value, 'utf-8')

然后将它下面的线改为:

value = '%s="%s"' % (name, value.decode('utf-8'))

这使得服务器(我已经测试过)接收字段并正确处理它。