Repository: aio-libs/aiohttp

Issue:
Documentation Error for Response parameters
Currently, the [documentation](http://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.Response) for

```
class aiohttp.web.Response(*, status=200, headers=None, content_type=None, charset=None, body=None, text=None)
```

states that

> content_type (str) – response’s content type. 'text/plain' if text is passed also, 'application/octet-stream' otherwise.

However, if I do not set the `text` argument (but set the `body` argument instead) the `content_type` is not set at all (whereas the documentation says it should be automatically set to `application/octet-stream`).

Python 3.5.1 and aiohttp 0.21.0



Discussion:
@asvetlov What is the intended behavior in this case?
I mean, is this a documentation error or implementation error?

It's implementation bug.
Would you create  a  Pull Request for fixing it?
Default `content-type` for prepared response should be `application/octet-stream` if the content type was not specified.

Sure thing


Resolving patch:
diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py
--- a/aiohttp/web_reqrep.py
+++ b/aiohttp/web_reqrep.py
@@ -417,6 +417,8 @@ def __init__(self, *, status=200, reason=None, headers=None):
 
         if headers is not None:
             self._headers.extend(headers)
+        self._parse_content_type(self._headers.get(hdrs.CONTENT_TYPE))
+        self._generate_content_type_header()
 
     def _copy_cookies(self):
         for cookie in self._cookies.values():
@@ -649,7 +651,7 @@ def _start_pre_check(self, request):
         if self._resp_impl is not None:
             if self._req is not request:
                 raise RuntimeError(
-                    'Response has been started with different request.')
+                    "Response has been started with different request.")
             else:
                 return self._resp_impl
         else:
@@ -728,7 +730,7 @@ def _start(self, request):
 
     def write(self, data):
         assert isinstance(data, (bytes, bytearray, memoryview)), \
-            'data argument must be byte-ish (%r)' % type(data)
+            "data argument must be byte-ish (%r)" % type(data)
 
         if self._eof_sent:
             raise RuntimeError("Cannot call write() after write_eof()")
@@ -774,19 +776,25 @@ def __init__(self, *, body=None, status=200,
         self.set_tcp_cork(True)
 
         if body is not None and text is not None:
-            raise ValueError("body and text are not allowed together.")
+            raise ValueError("body and text are not allowed together")
+
+        content_type_header = False
+        if headers is not None:
+            search_key = hdrs.CONTENT_TYPE.lower()
+            content_type_header = any(search_key == key.lower()
+                                      for key in headers)
 
         if text is not None:
-            if hdrs.CONTENT_TYPE not in self.headers:
+            if not content_type_header:
                 # fast path for filling headers
                 if not isinstance(text, str):
-                    raise TypeError('text argument must be str (%r)' %
+                    raise TypeError("text argument must be str (%r)" %
                                     type(text))
                 if content_type is None:
                     content_type = 'text/plain'
                 elif ";" in content_type:
-                    raise ValueError('charset must not be in content_type '
-                                     'argument')
+                    raise ValueError("charset must not be in content_type "
+                                     "argument")
                 charset = charset or 'utf-8'
                 self.headers[hdrs.CONTENT_TYPE] = (
                     content_type + '; charset=%s' % charset)
@@ -794,25 +802,22 @@ def __init__(self, *, body=None, status=200,
                 self._content_dict = {'charset': charset}
                 self.body = text.encode(charset)
             else:
-                self.text = text
                 if content_type or charset:
-                    raise ValueError("Passing both Content-Type header and "
+                    raise ValueError("passing both Content-Type header and "
                                      "content_type or charset params "
                                      "is forbidden")
+                self.text = text
         else:
-            if hdrs.CONTENT_TYPE in self.headers:
+            if content_type_header:
                 if content_type or charset:
-                    raise ValueError("Passing both Content-Type header and "
+                    raise ValueError("passing both Content-Type header and "
                                      "content_type or charset params "
                                      "is forbidden")
             if content_type:
                 self.content_type = content_type
             if charset:
                 self.charset = charset
-            if body is not None:
-                self.body = body
-            else:
-                self.body = None
+            self.body = body
 
     @property
     def body(self):
@@ -821,7 +826,7 @@ def body(self):
     @body.setter
     def body(self, body):
         if body is not None and not isinstance(body, bytes):
-            raise TypeError('body argument must be bytes (%r)' % type(body))
+            raise TypeError("body argument must be bytes (%r)" % type(body))
         self._body = body
         if body is not None:
             self.content_length = len(body)
@@ -837,7 +842,7 @@ def text(self):
     @text.setter
     def text(self, text):
         if text is not None and not isinstance(text, str):
-            raise TypeError('text argument must be str (%r)' % type(text))
+            raise TypeError("text argument must be str (%r)" % type(text))
 
         if self.content_type == 'application/octet-stream':
             self.content_type = 'text/plain'
@@ -865,7 +870,7 @@ def json_response(data=_sentinel, *, text=None, body=None, status=200,
     if data is not _sentinel:
         if text or body:
             raise ValueError(
-                'only one of data, text, or body should be specified'
+                "only one of data, text, or body should be specified"
             )
         else:
             text = dumps(data)


Test patch:
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -450,7 +450,8 @@ def handler(request):
     app.router.add_route('GET', '/', handler)
     resp = yield from client.get('/')
     assert resp.status == 200
-    assert resp.raw_headers == ((b'CONTENT-LENGTH', b'0'),
+    assert resp.raw_headers == ((b'CONTENT-TYPE', b'application/octet-stream'),
+                                (b'CONTENT-LENGTH', b'0'),
                                 (b'DATE', mock.ANY),
                                 (b'SERVER', mock.ANY))
     resp.close()
diff --git a/tests/test_web_response.py b/tests/test_web_response.py
--- a/tests/test_web_response.py
+++ b/tests/test_web_response.py
@@ -691,7 +691,9 @@ def test_response_ctor():
     assert 'OK' == resp.reason
     assert resp.body is None
     assert 0 == resp.content_length
-    assert CIMultiDict([('CONTENT-LENGTH', '0')]) == resp.headers
+    assert (CIMultiDict([('CONTENT-TYPE', 'application/octet-stream'),
+                         ('CONTENT-LENGTH', '0')]) ==
+            resp.headers)
 
 
 def test_ctor_with_headers_and_status():
@@ -700,7 +702,9 @@ def test_ctor_with_headers_and_status():
     assert 201 == resp.status
     assert b'body' == resp.body
     assert 4 == resp.content_length
-    assert (CIMultiDict([('AGE', '12'), ('CONTENT-LENGTH', '4')]) ==
+    assert (CIMultiDict([('AGE', '12'),
+                         ('CONTENT-TYPE', 'application/octet-stream'),
+                         ('CONTENT-LENGTH', '4')]) ==
             resp.headers)
 
 
@@ -816,8 +820,11 @@ def append(data):
     yield from resp.prepare(req)
     yield from resp.write_eof()
     txt = buf.decode('utf8')
-    assert re.match('HTTP/1.1 200 OK\r\nContent-Length: 0\r\n'
-                    'Date: .+\r\nServer: .+\r\n\r\n', txt)
+    assert re.match('HTTP/1.1 200 OK\r\n'
+                    'Content-Type: application/octet-stream\r\n'
+                    'Content-Length: 0\r\n'
+                    'Date: .+\r\n'
+                    'Server: .+\r\n\r\n', txt)
 
 
 @asyncio.coroutine
@@ -838,8 +845,12 @@ def append(data):
     yield from resp.prepare(req)
     yield from resp.write_eof()
     txt = buf.decode('utf8')
-    assert re.match('HTTP/1.1 200 OK\r\nContent-Length: 4\r\n'
-                    'Date: .+\r\nServer: .+\r\n\r\ndata', txt)
+    assert re.match('HTTP/1.1 200 OK\r\n'
+                    'Content-Type: application/octet-stream\r\n'
+                    'Content-Length: 4\r\n'
+                    'Date: .+\r\n'
+                    'Server: .+\r\n\r\n'
+                    'data', txt)
 
 
 @asyncio.coroutine
@@ -861,9 +872,12 @@ def append(data):
     yield from resp.prepare(req)
     yield from resp.write_eof()
     txt = buf.decode('utf8')
-    assert re.match('HTTP/1.1 200 OK\r\nContent-Length: 0\r\n'
+    assert re.match('HTTP/1.1 200 OK\r\n'
+                    'Content-Type: application/octet-stream\r\n'
+                    'Content-Length: 0\r\n'
                     'Set-Cookie: name=value\r\n'
-                    'Date: .+\r\nServer: .+\r\n\r\n', txt)
+                    'Date: .+\r\n'
+                    'Server: .+\r\n\r\n', txt)
 
 
 def test_set_text_with_content_type():
@@ -887,6 +901,26 @@ def test_set_text_with_charset():
     assert "koi8-r" == resp.charset
 
 
+def test_default_content_type_in_stream_response():
+    resp = StreamResponse()
+    assert resp.content_type == 'application/octet-stream'
+
+
+def test_default_content_type_in_response():
+    resp = Response()
+    assert resp.content_type == 'application/octet-stream'
+
+
+def test_content_type_with_set_text():
+    resp = Response(text='text')
+    assert resp.content_type == 'text/plain'
+
+
+def test_content_type_with_set_body():
+    resp = Response(body=b'body')
+    assert resp.content_type == 'application/octet-stream'
+
+
 def test_started_when_not_started():
     resp = StreamResponse()
     assert not resp.prepared