为啥在使用 multipart/form-data 时不能正确发送带有 Unicode 的 POST 名称?

Posted

技术标签:

【中文标题】为啥在使用 multipart/form-data 时不能正确发送带有 Unicode 的 POST 名称?【英文标题】:Why aren't POST names with Unicode sent correctly when using multipart/form-data?为什么在使用 multipart/form-data 时不能正确发送带有 Unicode 的 POST 名称? 【发布时间】:2014-01-02 16:52:43 【问题描述】:

我想发送一个附有文件的 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'

我该如何解决这个问题?

【问题讨论】:

对于这种事情,我个人喜欢在我过多地尝试一端或另一端之前查看并查看真正通过网络发送的内容。 Wireshark 或 tcpdump 应该提供一些见解。 字段值在 Wireshark 中显示为form-data;name*=utf-8''%5Cu2603。我不确定这有什么帮助。 与它在有效情况下的外观相比如何? 只是%5Cu2603=hello,因为它只是x-www-form-urlencoded。 multipart/form-data 是出于某种原因的问题,我不知道为什么。 也许是我的错,因为I introduced that RFC 2231 format into urllib3 在阅读了RFC 2388 Section 4.4 之后。由于this comment 指出the current html5 draft explicitely rejects RFC2231,我不再确定任何事情。我们有相互冲突的标准,服务器实现的完全是另一回事。 【参考方案1】:

从 wireshark cmets 看来,python-requests 做错了,但可能没有“正确答案”。

RFC 2388 说

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

RFC 2047 反过来说

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

并继续描述“Q”和“B”编码方法。使用“Q”(引用打印)方法,名称将是:

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

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

“编码词”不得用于 MIME Con​​tent-Type 或 Content-Disposition 字段的参数中,也不得用于除“评论”或“短语”之外的任何结构化字段正文中。

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

RFC 2388 也说

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

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

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

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

也就是说,如果采用这种方法(并且两端都支持),那么名称应该是:

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

幸运的是,RFC 5987 为 HTTP 标头添加了一个基于 RFC 2231 的编码! (此发现感谢@bobince)它说您可以(任何可能应该)包含 RFC 2231 样式的值一个普通值:

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

例子:

富:酒吧; title="欧元汇率"; 标题*=utf-8''%e2%82%ac%20exchange%20rates

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

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

另见:

HTTP headers encoding/decoding in Java How can I encode a filename according to RFC 2231? How to encode the filename parameter of Content-Disposition header in HTTP?

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

【讨论】:

这是一个很棒的答案,但似乎与我对 RFC 的阅读不符。您说属性中是否允许使用 RFC 2047 编码字是“不清楚的”:它不是,因为 RFC 6266 明确指出:“不得在 MIME Con​​tent-Type 或 Content-Disposition 的参数中使用‘编码字’字段或任何结构化字段正文中,“评论”或“短语”内除外。” RFC 2231 在这里是正确的:我对标准库为什么做得不好很感兴趣。 好吧,看来我把责任推得太早了:标准库正在做正确的事情。返回 urllib3。 RFC 2231 看起来是正确的,但我没有找到在 RFC 2388 中将其用于name 的权限......帮我把这些点联系起来?或者它可能在以后的 HTTP RFC 中? 我也没有看到任何明确的许可,尽管它也没有像 RFC 2047 那样明确排除。这可能是它支持不佳的原因:gunicorn 似乎根本不喜欢它。 已编辑以包含您对 RFC 6266 的看法 - 谢谢!【参考方案2】:

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

这里有两件事。

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

    如前所述,这是 RFC 2231 形式的扩展 Unicode 标头:

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 表格可能仍然会使一些旧服务器感到困惑。

【讨论】:

感谢 RFC 5987 的发现!我稍微编辑了我的答案以包含它。 文件名使用 RFC 2231 是在嵌套的 MIME 标头内,但在 HTTP body 内。所以 RFC 5987 并不适用。但是,服务器被混淆的事实当然是真实的,而不仅仅是旧的。 :-(【参考方案3】:

我想我应该为 urllib3 和 Requests 生成它所使用的格式这一事实负责。当I wrote that code 时,我主要考虑文件名,RFC 2388 section 4.5 建议在那里使用RFC 2231 格式。

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

最近我made aware 知道the current draft for the HTML 5 standard 有a section on the encoding of multipart/form-data。它与其他几个标准相矛盾,但它可能是未来。关于字段名(不是文件名),它描述了一种将字符转换为十进制 XML 实体的编码,例如☃ 为你的 雪人。但是,仅当为提交建立的编码不包含相关字符时才应应用该编码,而您的设置中不应出现这种情况。

我已提交an issue for urllib3 讨论此问题的后果,并可能在实施中解决这些问题。

【讨论】:

【参考方案4】:

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'))

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

【讨论】:

.decode('utf-8') 似乎不对,因为在 Python 2 上,我们的目标应该是返回一个字节字符串,而在 3 上,value 可能不是字节字符串(所以 decode 会失败) . (我不确定 urllib 混合 2 和 3 字符串类型的尝试在这里是否真的有效!)用 return '%s="%s"' % (name, value) 替换整个函数可能更简单...

以上是关于为啥在使用 multipart/form-data 时不能正确发送带有 Unicode 的 POST 名称?的主要内容,如果未能解决你的问题,请参考以下文章

为啥上传文件要使用multipart/form-data

为啥我的“multipart/form-data”标头不能阻止发送预检 (OPTIONS) 请求?

C# Multipart/Form-Data:为啥 PDF 文件上传没有问题但 JPG 失败?

html中表单使用post方式提交,为啥没有值

为啥不总是预检 POST 请求?

[转]如何使用multipart/form-data格式上传文件