我有一些Python代码使用requests库发出请求,偶尔会遇到IncompleteRead
错误。我正在尝试更新此代码以更优雅地处理此错误,并且想测试它是否有效,所以我想知道如何实际触发IncompleteRead
引发的条件。
我意识到我可以在单元测试中做一些嘲弄;我只是想实际重现以前发生此错误的情况(如果可以),并确保我的代码能够正确处理它。
答案 0 :(得分:1)
在测试依赖于外部行为的代码(例如服务器响应,系统传感器等)时,通常的方法是伪造外部因素而不是生成它们。
创建用于发出HTTP请求的函数或类的测试版本。如果您在代码库中直接使用requests
,请停止:与库和外部服务的直接耦合非常难以测试。
你提到你想确保你的代码可以处理这个异常,你宁愿避免因为这个原因而嘲笑。 Mocking同样安全,只要您在模块中包装模拟所需的模块。如果您无法模拟测试,那么您的设计中缺少图层(或者对测试套件要求太多)。
所以,例如:
class FooService(object):
def make_request(*args):
# use requests.py to perform HTTP requests
# NOBODY uses requests.py directly without passing through here
class MockFooService(FooService):
def make_request(*args):
raise IncompleteRead()
第二类是一个测试实用程序,仅用于测试此特定情况。随着测试的覆盖范围和完整性的增加,您可能需要更复杂的语言(以避免不断的子类化和重复),但通常最好从最简单的代码开始,这些代码可以轻松读取并测试所需的案例。
答案 1 :(得分:1)
这次添加第二个答案,更多。我深入研究了一些源代码,并找到了可能有用的信息
IncompleteRead
异常从httplib
冒出来,它是python标准库的一部分。最有可能的是,它来自this function:
def _safe_read(self, amt):
"""
Read the number of bytes requested, compensating for partial reads.
Normally, we have a blocking socket, but a read() can be interrupted
by a signal (resulting in a partial read).
Note that we cannot distinguish between EOF and an interrupt when zero
bytes have been read. IncompleteRead() will be raised in this
situation.
This function should be used when <amt> bytes "should" be present for
reading. If the bytes are truly not available (due to EOF), then the
IncompleteRead exception can be used to detect the problem.
"""
因此,在消耗HTTP响应之前,套接字是关闭的,或者读者试图从中获取太多字节。根据搜索结果来判断(所以用一粒盐来衡量),没有其他可以实现这一目标的神秘局面。
可以使用strace
调试第一个方案。如果我正确读取此内容,则第二种情况可能由requests
模块引起,如果:
Content-Length
标头超过服务器发送的实际数据量。此函数引发Exception
:
def _update_chunk_length(self):
# First, we'll figure out length of a chunk and then
# we'll try to read it from socket.
if self.chunk_left is not None:
return
line = self._fp.fp.readline()
line = line.split(b';', 1)[0]
try:
self.chunk_left = int(line, 16)
except ValueError:
# Invalid chunked protocol response, abort.
self.close()
raise httplib.IncompleteRead(line)
尝试检查缓冲响应的Content-Length
标头或分块响应的块格式。
要生成错误,请尝试:
Content-Length
答案 2 :(得分:0)
通过查看raise IncompleteRead
在https://github.com/python/cpython/blob/v3.8.0/Lib/http/client.py出现的位置,我认为标准库的http.client
模块(在Python 2中更名为httplib
)仅引发了此异常以下两种情况:
Content-Length
标头声明的正文短时,或者如果安装了Flask(pip install Flask
),则可以将其粘贴到文件中以创建测试服务器,并可以通过人为地创建这两种情况的端点来运行该服务器:
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/test')
def send_incomplete_response():
response = make_response('fourteen chars')
response.headers['Content-Length'] = '10000'
return response
@app.route('/test_chunked')
def send_chunked_response_with_wrong_sizes():
# Example response based on
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding
# but with the stated size of the second chunk increased to 900
resp_text = """7\r\nMozilla\r\n900\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n"""
response = make_response(resp_text)
response.headers['Transfer-Encoding'] = 'chunked'
return response
app.run()
然后用http.client
测试它们:
>>> import http.client
>>>
>>> conn = http.client.HTTPConnection('localhost', 5000)
>>> conn.request('GET', '/test')
>>> response = conn.getresponse()
>>> response.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/http/client.py", line 467, in read
s = self._safe_read(self.length)
File "/usr/lib/python3.8/http/client.py", line 610, in _safe_read
raise IncompleteRead(data, amt-len(data))
http.client.IncompleteRead: IncompleteRead(14 bytes read, 9986 more expected)
>>>
>>> conn = http.client.HTTPConnection('localhost', 5000)
>>> conn.request('GET', '/test_chunked')
>>> response = conn.getresponse()
>>> response.read()
Traceback (most recent call last):
File "/usr/lib/python3.8/http/client.py", line 571, in _readall_chunked
value.append(self._safe_read(chunk_left))
File "/usr/lib/python3.8/http/client.py", line 610, in _safe_read
raise IncompleteRead(data, amt-len(data))
http.client.IncompleteRead: IncompleteRead(28 bytes read, 2276 more expected)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/http/client.py", line 461, in read
return self._readall_chunked()
File "/usr/lib/python3.8/http/client.py", line 575, in _readall_chunked
raise IncompleteRead(b''.join(value))
http.client.IncompleteRead: IncompleteRead(7 bytes read)
在现实生活中,偶尔发生这种情况的最可能原因是服务器是否提前关闭了连接。例如,您还可以尝试运行 this Flask服务器,该服务器发送响应主体非常慢,总共睡眠20秒:
from flask import Flask, make_response, Response
from time import sleep
app = Flask(__name__)
@app.route('/test_generator')
def send_response_with_delays():
def generate():
yield 'foo'
sleep(10)
yield 'bar'
sleep(10)
yield 'baz'
response = Response(generate())
response.headers['Content-Length'] = '9'
return response
app.run()
如果您在终端中运行该服务器,则向该服务器发起请求并开始读取响应,如下所示...
>>> import http.client
>>> conn = http.client.HTTPConnection('localhost', 5000)
>>> conn.request('GET', '/test_generator')
>>> response = conn.getresponse()
>>> response.read()
...,然后飞回运行服务器的终端并杀死它(例如,在Unix上使用CTRL-C),那么您将看到.read()
呼叫错误,并显示一条熟悉的消息:< / p>
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/http/client.py", line 467, in read
s = self._safe_read(self.length)
File "/usr/lib/python3.8/http/client.py", line 610, in _safe_read
raise IncompleteRead(data, amt-len(data))
http.client.IncompleteRead: IncompleteRead(6 bytes read, 3 more expected)
其他不太可能的原因包括服务器系统生成不正确的Content-Length
标头(可能是由于对unicode的某些破坏)或Content-Length
标头(或{{1中包含的长度) }}邮件)在传输过程中被破坏。
好的,它涵盖了标准库。那么请求呢?默认情况下,请求会将其工作推迟到chunked
,而这又会推迟到urllib3
,因此您可能期望http.client
中的异常在使用请求时会突然冒泡。但是,生活要复杂得多,有两个原因:
http.client
和urllib3
都在它们下面的层中捕获异常并引发它们自己的版本。例如,有requests
和urllib3.exceptions.IncompleteRead
。
目前requests.exceptions.ChunkedEncodingError
检查所有这三个模块的操作非常糟糕,并且已经进行了多年。如果您有兴趣,我会尽力在https://github.com/psf/requests/issues/4956#issuecomment-573325001上进行详细说明,但简短的版本是Content-Length
不会在您致电{{1}时检查http.client
},而不只是Content-Length
,.read(123)
可能会或可能不会检查,这取决于您如何调用它的各种复杂细节,并且由于前两个问题,请求目前并未检查从来没有。但是,情况并非总是如此。有人尝试修复它的成败,所以也许在过去的某个时候(例如在2016年问这个问题时),游戏的状态有些不同。哦,还有一个更令人困惑的地方,尽管.read()
有其自己的版本,但有时 仍会使标准库的urllib3
异常冒出气泡,以至于惹上您。
希望第二点会及时解决-我现在正朝着那个方向轻推。要点1仍然很复杂,但是触发这些异常的条件-无论是基础的urllib3
还是IncompleteRead
或http.client.IncompleteRead
的替代方案-都应该像我在回答开始时所描述的那样保持。