使用 Python 创建和解析多部分 HTTP 请求

9

我正在尝试编写一些Python代码,可以在客户端创建多部分MIME HTTP请求,并在服务器上正确解释它们。我认为,客户端方面的代码已经部分成功,代码如下:

from email.mime.multipart import MIMEMultipart, MIMEBase
import httplib
h1 = httplib.HTTPConnection('localhost:8080')
msg = MIMEMultipart()
fp = open('myfile.zip', 'rb')
base = MIMEBase("application", "octet-stream")
base.set_payload(fp.read())
msg.attach(base)
h1.request("POST", "http://localhost:8080/server", msg.as_string())

这样做的唯一问题在于邮件库还包括Content-Type和MIME-Version头,我不确定它们如何与httplib包含的HTTP头相关联:
Content-Type: multipart/mixed; boundary="===============2050792481=="
MIME-Version: 1.0

--===============2050792481==
Content-Type: application/octet-stream
MIME-Version: 1.0

这可能是我的web.py应用程序接收到此请求时出现错误消息的原因。web.py POST处理程序:

class MultipartServer:
    def POST(self, collection):
        print web.input()

抛出此错误:
Traceback (most recent call last):
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 242, in process
    return self.handle()
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 233, in handle
    return self._delegate(fn, self.fvars, args)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 415, in _delegate
    return handle_class(cls)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 390, in handle_class
    return tocall(*args)
  File "/home/richard/Development/server/webservice.py", line 31, in POST
    print web.input()
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/webapi.py", line 279, in input
    return storify(out, *requireds, **defaults)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 150, in storify
    value = getvalue(value)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 139, in getvalue
    return unicodify(x)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 130, in unicodify
    if _unicode and isinstance(s, str): return safeunicode(s)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 326, in safeunicode
    return obj.decode(encoding)
  File "/usr/lib/python2.6/encodings/utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 137-138: invalid data

我的代码行被错误行代表,大约在中间位置:

  File "/home/richard/Development/server/webservice.py", line 31, in POST
    print web.input()

进展不错,但我不确定接下来该做什么。这是我的客户端代码问题还是web.py的限制(可能它无法支持多部分请求)?如果有任何提示或建议可以提供替代代码库,将不胜感激。

编辑

上述错误是因为数据没有自动进行base64编码导致的。添加

encoders.encode_base64(base)

解决了这个错误,现在问题变得清晰明了。服务器未能正确解释HTTP请求,可能是因为电子邮件库将应该包含在头部的HTTP头信息放在了正文中:

<Storage {'Content-Type: multipart/mixed': u'', 
          ' boundary': u'"===============1342637378=="\n'
          'MIME-Version: 1.0\n\n--===============1342637378==\n'
          'Content-Type: application/octet-stream\n'
          'MIME-Version: 1.0\n' 
          'Content-Transfer-Encoding: base64\n'
          '\n0fINCs PBk1jAAAAAAAAA.... etc

所以有些地方不对劲。

谢谢

Richard


1
@TokenMacGuy - 是的。是的,没错。 - Richard J
3个回答

1

经过一番探索,这个问题的答案已经明确了。简单来说,虽然在Mime编码的消息中Content-Disposition是可选的,但是Web.py需要对每个Mime部分都使用它才能正确解析HTTP请求。

与其他评论不同,HTTP和电子邮件之间的区别并不重要,因为它们只是Mime消息的传输机制,仅此而已。多部分/相关(而不是多部分/表单数据)消息在内容交换Web服务中很常见,这就是我们的用例。提供的代码片段是准确的,虽然指导我找到了一个稍微简洁的解决方案。

# open an HTTP connection
h1 = httplib.HTTPConnection('localhost:8080')

# create a mime multipart message of type multipart/related
msg = MIMEMultipart("related")

# create a mime-part containing a zip file, with a Content-Disposition header
# on the section
fp = open('file.zip', 'rb')
base = MIMEBase("application", "zip")
base['Content-Disposition'] = 'file; name="package"; filename="file.zip"'
base.set_payload(fp.read())
encoders.encode_base64(base)
msg.attach(base)

# Here's a rubbish bit: chomp through the header rows, until hitting a newline on
# its own, and read each string on the way as an HTTP header, and reading the rest
# of the message into a new variable
header_mode = True
headers = {}
body = []
for line in msg.as_string().splitlines(True):
    if line == "\n" and header_mode == True:
        header_mode = False
    if header_mode:
        (key, value) = line.split(":", 1)
        headers[key.strip()] = value.strip()
    else:
        body.append(line)
body = "".join(body)

# do the request, with the separated headers and body
h1.request("POST", "http://localhost:8080/server", body, headers)

web.py可以完美地处理这个问题,因此email.mime.multipart非常适合创建通过HTTP传输的Mime消息,但它的头部处理除外。

我的另一个总体关注点是可扩展性。既然这个解决方案和其他在这里提出的解决方案都会将文件内容读入变量中,然后再打包到mime消息中,所以它们都不具备良好的可扩展性。更好的解决方案应该是能够按需序列化,随着内容通过HTTP连接进行传输而实现。我目前没有紧急修复这个问题的计划,但如果我有解决方案,我会回来分享的。


  1. 我认为设置标题的首选方式类似于 base.add_header('Content-Disposition','file',name='package',...)
  2. 最好搜索 \n\n(以及 \r?\n\r?\n,例如使用 re.search('\r?\n\r?\n',...)),这样您就不必拆分和连接正文。
  3. 标题行可以折叠。
  4. 从技术上讲,终止标题的 \n 不是正文的一部分,尽管这并不会造成任何损害。
  5. 我不完全确定 RFC 5322 和 RFC 2316 语法是否百分之百兼容(特别是关于“字符”与八位字节)。
- tc.

1

太好了,谢谢,我会看一下的。这个名字确实很合适 :) 干杯,R - Richard J

-1

您的请求存在一些问题。如TokenMacGuy所建议,HTTP中未使用multipart/mixed,请改用multipart/form-data。此外,部分应该有Content-disposition头信息。可以在Code Recipes中找到执行此操作的Python片段。


谢谢 - 正如您所看到的,我仍在努力;我还没有弄清楚multipart/mixed在哪里设置。同样,我还没有使用Content-Disposition头部,因为我还在努力将其首先放入HTTP请求中。我的问题是关于如何构建这样一个请求。祝好,R. - Richard J
看看这个配方。忘记email.mime - HTTP不是电子邮件。 - Martin v. Löwis
嗨,马丁;FYI,我已经证明了HTTP和电子邮件之间的差异在这里是无关紧要的 - 它们只是传输方式,而MIME在任何情况下都是相同的。请查看我的替代答案。感谢你的指引。R - Richard J
2
这是错误的,multipart/mixed 在 HTTP 中使用:http://docs.couchdb.org/en/latest/replication/protocol.html#fetch-changed-documents - fiatjaf
即使很少使用,Python中也会使用multipart/mixed。请参阅Google APIs:https://developers.google.com/drive/api/v3/performance#details - Lorenzo Persichetti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接