Skip to content

Commit 35196f8

Browse files
committed
Send raw files as is
This fixes the behavior when on attempt to POST file object or BytesIO aiohttp instead of sending their content as is wraps them with multipart request what isn't welcome for most cases. This commit changes that behavior: if you call aiohttp.request with data parameter as file or BytesIO, it will send them as raw binary chunked stream. In case of files, when Content-Type header isn't set aiohttp will try to guess the mimetype using the mimetypes module. Otherwise default 'application/octet-stream' will be used. There are also checks for StringIO and files opened in text mode: both aren't allowed since they may cause to send data using not the encoding you expected to.
1 parent 6ed78f1 commit 35196f8

File tree

3 files changed

+55
-11
lines changed

3 files changed

+55
-11
lines changed

aiohttp/client.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import asyncio
66
import collections
77
import http.cookies
8-
import json
9-
import io
108
import inspect
9+
import io
10+
import json
11+
import mimetypes
1112
import random
1213
import time
1314
import urllib.parse
@@ -373,6 +374,20 @@ def update_body_from_data(self, data):
373374
if 'CONTENT-LENGTH' not in self.headers and self.chunked is None:
374375
self.chunked = True
375376

377+
elif isinstance(data, io.IOBase):
378+
assert not isinstance(data, io.StringIO), \
379+
'attempt to send text data instead of binary'
380+
self.body = data
381+
self.chunked = True
382+
if hasattr(data, 'mode'):
383+
if data.mode == 'r':
384+
raise ValueError('file {!r} should be open in binary mode'
385+
''.format(data))
386+
if 'CONTENT-TYPE' not in self.headers and hasattr(data, 'name'):
387+
mime = mimetypes.guess_type(data.name)[0]
388+
mime = 'application/octet-stream' if mime is None else mime
389+
self.headers['CONTENT-TYPE'] = mime
390+
376391
else:
377392
if not isinstance(data, helpers.FormData):
378393
data = helpers.FormData(data)
@@ -474,6 +489,12 @@ def write_bytes(self, request, reader):
474489
except streams.EofStream:
475490
break
476491

492+
elif isinstance(self.body, io.IOBase):
493+
chunk = self.body.read(self.chunked)
494+
while chunk:
495+
request.write(chunk)
496+
chunk = self.body.read(self.chunked)
497+
477498
else:
478499
if isinstance(self.body, (bytes, bytearray)):
479500
self.body = (self.body,)

tests/test_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import asyncio
55
import inspect
6+
import io
67
import time
78
import unittest
89
import unittest.mock
@@ -634,6 +635,24 @@ def gen():
634635
unittest.mock.call(b'\r\n'),
635636
unittest.mock.call(b'0\r\n\r\n')])
636637

638+
def test_data_file(self):
639+
req = ClientRequest(
640+
'POST', 'http://python.org/', data=io.BytesIO(b'*' * 2),
641+
loop=self.loop)
642+
self.assertTrue(req.chunked)
643+
self.assertTrue(isinstance(req.body, io.IOBase))
644+
self.assertEqual(req.headers['TRANSFER-ENCODING'], 'chunked')
645+
646+
resp = req.send(self.transport, self.protocol)
647+
self.assertIsInstance(req._writer, asyncio.Future)
648+
self.loop.run_until_complete(resp.wait_for_close())
649+
self.assertIsNone(req._writer)
650+
self.assertEqual(
651+
self.transport.write.mock_calls[-3:],
652+
[unittest.mock.call(b'*' * 2),
653+
unittest.mock.call(b'\r\n'),
654+
unittest.mock.call(b'0\r\n\r\n')])
655+
637656
def test_data_stream_exc(self):
638657
fut = asyncio.Future(loop=self.loop)
639658

tests/test_client_functional.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -470,21 +470,25 @@ def test_POST_FILES_SINGLE(self):
470470
url = httpd.url('method', 'post')
471471

472472
with open(__file__) as f:
473+
with self.assertRaises(ValueError):
474+
self.loop.run_until_complete(
475+
client.request('post', url, data=f, loop=self.loop))
476+
477+
def test_POST_FILES_SINGLE_BINARY(self):
478+
with test_utils.run_server(self.loop, router=Functional) as httpd:
479+
url = httpd.url('method', 'post')
480+
481+
with open(__file__, 'rb') as f:
473482
r = self.loop.run_until_complete(
474483
client.request('post', url, data=f, loop=self.loop))
475484

476485
content = self.loop.run_until_complete(r.json())
477486

478487
f.seek(0)
479-
filename = os.path.split(f.name)[-1]
480-
481-
self.assertEqual(1, len(content['multipart-data']))
482-
self.assertEqual(
483-
filename, content['multipart-data'][0]['name'])
484-
self.assertEqual(
485-
filename, content['multipart-data'][0]['filename'])
486-
self.assertEqual(
487-
f.read(), content['multipart-data'][0]['data'])
488+
self.assertEqual(0, len(content['multipart-data']))
489+
self.assertEqual(content['content'], f.read().decode())
490+
self.assertEqual(content['headers']['Content-Type'],
491+
'text/x-python')
488492
self.assertEqual(r.status, 200)
489493
r.close()
490494

0 commit comments

Comments
 (0)