Repository: aio-libs/aiohttp

Issue:
Raise an exception if trying to write into closed response
See https://stackoverflow.com/questions/47175297/handling-premature-client-disconnection-in-aiohttp

The problem is: `await drain()` does nothing because the buffer is not overflown, the buffer is not overflown because sent data are ignored and not accumulated in internal buffer.
`transport.is_closing()` should be used for check.

It should be fixed in payload writer, affects both client and server.


Discussion:
Although, adding 
```python
if self._transport.is_closing():
    return noop()
```
to StreamResponse.write() (this fixes both server and client I guess)
https://github.com/aio-libs/aiohttp/blob/feaf0213b926a3f6093cbec0217bb2239b2b0f25/aiohttp/http_writer.py#L200

fixes logging of errors and trying to write to closing transport, for loop continues to execute and tries unsuccessfully to write to the closing transport, that means:
* all further iterations do nothing - useless load on server
* unable to catch closing of transport

Moreover, looks like there is no foreseen way to indicate that `StreamResponse` is closed or not, except `self._eof`, which looks too internal for me to use it from outer scope
IMO there are several ways to fix this bug:
* add raising `asyncio.CancelledError`, as SO author proposed. This is too ugly I guess, raising an Exceptions is this situation is just a hack. Worth mentioning, that it is possible to catch CancelledError but not possible to catch ConnectionError, for example, that makes this way even more dirty
* add property (e.g. `is_closing`) with mapping to `self._transport.is_closing()` in `PayloadWriter` and another property for `StreamResponse` to indicate that there is still opened stream (e.g. `is_open`), but  this will create new API for this bug purpose only i guess (we need to pass `response._payload_writer._transport.is_closing()` to the user), that looks weird

I`m not sure what solution should be implemented, so just leave this message for further discussion
I've worked around this issue by using checking `request.protocol.transport.is_closing()` before calling `response.write()` in my handler. It seems to work well so far in my tests. Although I am not so sure if this is the right way to go about this.
Explicit exception in `response.write()` if `request.protocol.transport.is_closing()` is least surprising behavior IMHO.

Resolving patch:
diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py
--- a/aiohttp/http_writer.py
+++ b/aiohttp/http_writer.py
@@ -1,5 +1,6 @@
 """Http related parsers and protocol."""
 
+import asyncio
 import collections
 import socket
 import zlib
@@ -126,6 +127,9 @@ def _write(self, chunk):
         size = len(chunk)
         self.buffer_size += size
         self.output_size += size
+
+        if self._transport.is_closing():
+            raise asyncio.CancelledError('Cannot write to closing transport')
         self._transport.write(chunk)
 
     def write(self, chunk, *, drain=True, LIMIT=64*1024):


Test patch:
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -50,6 +50,7 @@ async def write_eof():
 
     transport.write.side_effect = write
     transport.write_eof.side_effect = write_eof
+    transport.is_closing.return_value = False
 
     return transport
 
diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py
--- a/tests/test_http_writer.py
+++ b/tests/test_http_writer.py
@@ -1,4 +1,5 @@
 """Tests for aiohttp/http_writer.py"""
+import asyncio
 import zlib
 from unittest import mock
 
@@ -20,6 +21,7 @@ def write(chunk):
         buf.extend(chunk)
 
     transport.write.side_effect = write
+    transport.is_closing.return_value = False
     return transport
 
 
@@ -147,3 +149,13 @@ def test_write_drain(stream, loop):
     msg.write(b'1', drain=True)
     assert msg.drain.called
     assert msg.buffer_size == 0
+
+
+def test_write_to_closing_transport(stream, loop):
+    msg = http.PayloadWriter(stream, loop)
+
+    msg.write(b'Before closing')
+    stream.transport.is_closing.return_value = True
+
+    with pytest.raises(asyncio.CancelledError):
+        msg.write(b'After closing')
diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py
--- a/tests/test_web_protocol.py
+++ b/tests/test_web_protocol.py
@@ -84,6 +84,7 @@ def write(chunk):
 
     transport.write.side_effect = write
     transport.drain.side_effect = helpers.noop
+    transport.is_closing.return_value = False
 
     return transport