urllib3: HTTPResponse.read() closes the response causing problem with io.BufferedReader
In the following text I mostly refer to python2 for clarity, but the problem also happens on py3.
From some library I get an active streaming urllib3.HTTPResponse with preload_content=False. That response refers to a very large CSV file, so I want to read it line-by-line passing it through csv.reader.
csv.reader requires unicode objects. HTTPResponse returns bytes objects, so I use io.TextIOWrapper. (Actually I use backports.csv to achieve compatibility with io).
Now, here is the problem:
test.csv - notice the last line is missing a trailing line separator:
abc<LF>
def
It is served with python -m SimpleHTTPServer.
And the test code, even without CSV:
import io
import urllib3
http = urllib3.PoolManager()
resp = http.request('GET', 'http://localhost:8000/test.csv', preload_content=False)
for line in iter(
io.TextIOWrapper(
# py2's implementation of TextIOWrapper requires `read1` method which is provided by `BufferedReader` wrapper
io.BufferedReader(
resp
)
)
):
print(repr(line))
Here is the error:
u'abc\n'
Traceback (most recent call last):
File "test.py", line 11, in <module>
resp
ValueError: I/O operation on closed file.
If I remove TextIOWrapper then the error is slightly different but with the same meaning:
ValueError: readline of closed file
From what I could find out, C implementation of readline checks if the file is closed. And if (and only if) the last line is not terminated then it tries to read “the rest” which results in this error, because HTTPResponse.read() closes the “file” after it is finished.
This behaviour is contrary to ordinary file behaviour (and io.open() or py3 file behaviour) which will not close the file on its will but instead will keep yielding empty string when trying to read past the end of file.
Which would be the best way to make streaming HTTPResponse work properly with io.BufferedReader?
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Comments: 15 (12 by maintainers)
Okay, so what we have here is an API mismatch. An HTTP response body can generally be treated as a file which…
iomodule via the.closedproperty)Most of these attributes are perfectly normal and expected for a file-like object. However, self-closing is not.
Generally, a file is open when it can be interacted with. When it’s closed, a file pointer raises exceptions when it’s interacted with. Thus, we have a bit of a consistency problem. We can (and should) be able to interact with an HTTPResponse after its body has been completely read, but we’re currently acting as though the file is closed immediately upon consumption.
Essentially, we’re using
.closedto telegraph that a file has been consumed, but the standard behavior for a (read-only, non-seekable) file which has been completely consumed (but which remains open) is to return an empty set of bytes. A file being closed has a semantically different meaning - it means that the file can no longer be interacted with in any way.I propose that we change the behavior of
.close()and.closedonHTTPResponseto more closely match the semantic meaning intended byIOBase. Calling.close()will result in a call to the underlyinghttplibresponse object’s.close()method, followed by a call tosuper().close(), while.closedwill simply inherit fromIOBase; if the response has not been specifically closed, it will appear open - which is the more conservative move, as it will never appear closed in a case where the httplib fp remains open (and therefore needs to be explicitly closed).This table summarizes behavior before and after this change:
close()closed@nateprewitt, you’ve investigated this in the past in #977. What are your thoughts? Also CC @sigmavirus24 and @Lukasa.
Lots of good information and investigation here, nice work @haikuginger. You know when tables are used in a GitHub issue comment that shit’s getting serious. 😃
I’m in favor of the change as long as it goes in as a breaking change.
So that attribute would be used like so:
I’d be open to a PR for this change. 😃
@gsakkis, here is the workaround I used in my project:
Hope it helps 😃
@nateprewitt, understood. In general, though, since we don’t ourselves rely on the property, do actively close the connection ourselves, and provide
.closedas a convenience for compatibility withio, I’m leaning towards not worrying about that potential at the moment.I’m adding two issue labels - “solution proposed” and “proposed solution accepted” and tagging this issue with the first; if there are no significant objections in the next couple days, I’ll move it to the latter to indicate that any contributor or myself may begin work on the proposed change.