diff --git a/feedparser/api.py b/feedparser/api.py
index b11e4a47..af7a77cc 100644
--- a/feedparser/api.py
+++ b/feedparser/api.py
@@ -108,7 +108,10 @@ def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, h
"""
if hasattr(url_file_stream_or_string, 'read'):
- return url_file_stream_or_string.read()
+ data = url_file_stream_or_string.read()
+ if not isinstance(data, bytes):
+ return data.encode('utf-8')
+ return data
if isinstance(url_file_stream_or_string, str) \
and urllib.parse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp', 'file', 'feed'):
diff --git a/tests/runtests.py b/tests/runtests.py
index ad45e70a..c1a5b0f1 100644
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -378,6 +378,22 @@ def test_fileobj(self):
r = feedparser.api._open_resource(io.BytesIO(b''), '', '', '', '', [], {}, {})
self.assertEqual(r, b'')
+ def test_text_fileobj(self):
+ """A text-mode file-like object (e.g. io.StringIO) must come back as bytes.
+
+ Everything downstream, starting with convert_to_utf8(), works with
+ raw bytes and matches against byte regex patterns, so returning the
+ str data untouched breaks parsing of any text-mode stream.
+ """
+ r = feedparser.api._open_resource(io.StringIO(''), '', '', '', '', [], {}, {})
+ self.assertEqual(r, b'')
+
+ def test_parse_text_stringio(self):
+ text = '- hello
'
+ result = feedparser.parse(io.StringIO(text))
+ self.assertFalse(result.bozo)
+ self.assertEqual(result.entries[0].title, 'hello')
+
def test_feed(self):
f = feedparser.parse('feed://localhost:8097/tests/http/target.xml')
self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')