httplib 的行为(至少在 Python 2.7 中)是这样的,它会在添加自己的标头之前检查是否存在现有的 Content-Length 标头,因此,如果您碰巧知道内容的大小,那么您可以添加您自己的标题 - 例如:
c.request("POST", "/xpost", s, headers={"Content-Length": len(s.getvalue())})
如果没有这样的标头,httplib 会尝试调用 len() 自动填写一个,如果失败,它会假定正文必须是类文件对象并在操作系统级别调用 os.fstat()文件描述符以确定其大小 - 它通过在您给它的文件句柄上调用 fileno() 方法来获取此描述符。这适用于真实文件,但由于StringIO 对象不是真实文件,它们不提供fileno() 方法并且操作失败并返回AttributeError。这个错误被httplib捕获并静默处理,它根本无法添加Content-Length。
如果您确定使用StringIO 对象,您最简单的选择可能是添加您自己的Content-Length 标头,如我在上面的示例中所示。如果这只是一个测试,而您将在现实中使用真实文件,那么只要os.fstat() 在您的平台上工作,您就可以依靠httplib 正确设置标题。如果没有,您可以随时在文件名上自己调用os.stat(),并以同样的方式提供您自己的标题。
如果你想同时处理真实文件和StringIO,那么你总是可以这样做:
headers = {}
if not hasattr(body, "fileno"):
headers["Content-Length"] = len(body.getvalue())
...但我不建议您添加这种复杂性,除非您需要它。
最后,在 HTTP 级别还有另一个选项是使用 chunked encoding,您不需要提供 Content-Length 标头,主体本身被编码为自描述数据块。然而不幸的是,许多客户端和服务器(包括httplib)的HTTP 软件倾向于假设只有响应 会被分块,而请求 将始终使用Content-Length。我想这个假设是因为请求通常很小,但是对于POST 和PUT,这个假设当然不成立。
假设您确信您的服务器将处理分块请求,您可以尝试这样做 - 为此,您需要构建一个 StringIO 对象(或任何其他没有 fileno() 方法的对象来击败 @987654352 @ 的自动 Content-Length 插入)已经放置了分块编码,并为您自己的 Transfer-Encoding 标头提供值 chunked。如果您的软件旨在与各种服务器一起使用,我个人不建议您这样做。
编辑:顺便说一句,如果您使用分块编码,您不得发送Content-Length 标头 - 请参阅HTTP RFC §4.4 第 3 项。通过请求,当然,您不能通过简单地关闭连接来发出正文结束的信号,因为这样您就没有可以接收响应的连接。
作为对分块请求的支持有多差的一个例子,nginx 仅在去年年底将其添加到version 1.3.9 的核心功能中(尽管在那之前有a plugin for it)。
编辑 2:
如果您通读 Wikipedia 文章,您会发现除了发送正确的标头之外还有更多内容 - 您必须将正文拆分为多个块,并在每个块中发送一个包含该块大小的小标头十六进制。这通常会在发送响应时为您完成,但正如我在请求中提到的那样,对它的支持很差。
这是一个围绕类文件对象的包装器示例,它将主体转换为块。我已经修改了您上面的示例以展示如何使用它,尽管 "hello world" 主体当然很小,最终只能成为一个块。但是,它应该适用于任何大小的物体。它应该适用于具有read() 方法的任何对象,该方法与Python file 对象的工作方式相同。实际上,如果您将标准 Python 文件对象包装在其中之一中,它将阻止 httplib 添加 Content-Length,因为它不支持 len() 或 fileno()。
您仍然需要记住自己添加 Transfer-Encoding 标头,如下面的示例所示:
import httplib
import cStringIO
class ChunkedEncodingWrapper(object):
def __init__(self, fileobj, blocksize=8192):
self.fileobj = fileobj
self.blocksize = blocksize
self.current_chunk = ""
self.closed = False
def read(self, size=None):
ret = ""
while size is None or size >= len(self.current_chunk):
ret += self.current_chunk
if size is not None:
size -= len(self.current_chunk)
if self.closed:
self.current_chunk = ""
break
self._get_chunk()
else:
ret += self.current_chunk[:size]
self.current_chunk = self.current_chunk[size:]
return ret
def _get_chunk(self):
if not self.closed:
chunk = self.fileobj.read(self.blocksize)
if chunk:
self.current_chunk = "%x" % (len(chunk),) + "\r\n" + chunk + "\r\n"
else:
self.current_chunk = "0\r\n\r\n"
self.closed = True
s = cStringIO.StringIO("hello world")
w = ChunkedEncodingWrapper(s)
c = httplib.HTTPConnection("xxx.xxx.xxx.xxx")
c.request("POST", "/xpost", w, headers={"Transfer-Encoding": "chunked"})