Repository: django/daphne

Issue:
'WebRequest' object has no attribute 'client_addr'
Hi, I've recently migrated my Django project from WSGI + gunicorn to ASGI + daphne. It's working great apart from an occasional error in my Sentry/logs `builtins.AttributeError: 'WebRequest' object has no attribute 'client_addr'`.
It just seems to be the problem in the `log.debug` call on `connectionLost` in https://github.com/django/daphne/blob/main/daphne/http_protocol.py#L213
Looking at the class it seems that there is a chance that the `client_addr` is not set until later in the `process` method https://github.com/django/daphne/blob/main/daphne/http_protocol.py#L81 so there's a chance that the `self.client_addr` is simply not set. 

I can't replicate this error by any means, it happens only occasionally (I guess that's why it pops up in the `connectionLost` method). Should the `client_addr` be set to None by default so that this log message doesn't cause fatals?

BTW I've seen https://github.com/django/daphne/issues/304 and https://github.com/django/daphne/issues/244. First one is giving 404 on SO, second one seems similar but they're mentioning websockets whereas I haven't even got to the point where I'm supporting websockets in my app.

- Your OS and runtime environment, and browser if applicable
Presumably platform browser agnostic. I don't have a reproduction method.

- A `pip freeze` output showing your package versions

channels==3.0.3
channels_redis==3.3.0
daphne==3.0.2
Django==3.2.6

- How you're running Channels (runserver? daphne/runworker? Nginx/Apache in front?)

daphne on Heroku with `daphne my_app.asgi:application --port $PORT --bind 0.0.0.0 --verbosity 2` in Procfile

- Console logs and full tracebacks of any errors
```
 Jan 28 11:55:38 app/web.3 Unhandled Error
 Jan 28 11:55:38 app/web.3 Traceback (most recent call last):
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/internet/asyncioreactor.py", line 257, in run
 Jan 28 11:55:38 app/web.3     self._asyncioEventloop.run_forever()
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/asyncio/base_events.py", line 570, in run_forever
 Jan 28 11:55:38 app/web.3     self._run_once()
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/asyncio/base_events.py", line 1859, in _run_once
 Jan 28 11:55:38 app/web.3     handle._run()
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/asyncio/events.py", line 81, in _run
 Jan 28 11:55:38 app/web.3     self._context.run(self._callback, *self._args)
 Jan 28 11:55:38 app/web.3 --- <exception caught here> ---
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/python/log.py", line 101, in callWithLogger
 Jan 28 11:55:38 app/web.3     return callWithContext({"system": lp}, func, *args, **kw)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/python/log.py", line 85, in callWithContext
 Jan 28 11:55:38 app/web.3     return context.call({ILogContext: newCtx}, func, *args, **kw)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/python/context.py", line 118, in callWithContext
 Jan 28 11:55:38 app/web.3     return self.currentContext().callWithContext(ctx, func, *args, **kw)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/python/context.py", line 83, in callWithContext
 Jan 28 11:55:38 app/web.3     return func(*args, **kw)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/internet/asyncioreactor.py", line 145, in _readOrWrite
 Jan 28 11:55:38 app/web.3     self._disconnectSelectable(selectable, why, read)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/internet/posixbase.py", line 300, in _disconnectSelectable
 Jan 28 11:55:38 app/web.3     selectable.readConnectionLost(f)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/internet/tcp.py", line 308, in readConnectionLost
 Jan 28 11:55:38 app/web.3     self.connectionLost(reason)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/internet/tcp.py", line 325, in connectionLost
 Jan 28 11:55:38 app/web.3     protocol.connectionLost(reason)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/twisted/web/http.py", line 2508, in connectionLost
 Jan 28 11:55:38 app/web.3     request.connectionLost(reason)
 Jan 28 11:55:38 app/web.3   File "/app/.heroku/python/lib/python3.8/site-packages/daphne/http_protocol.py", line 213, in connectionLost
 Jan 28 11:55:38 app/web.3     logger.debug("HTTP disconnect for %s", self.client_addr)
 Jan 28 11:55:38 app/web.3 builtins.AttributeError: 'WebRequest' object has no attribute 'client_addr'
```


Discussion:

Patch:
diff --git a/daphne/http_protocol.py b/daphne/http_protocol.py
index 7df7bae..a289e93 100755
--- a/daphne/http_protocol.py
+++ b/daphne/http_protocol.py
@@ -50,6 +50,8 @@ class WebRequest(http.Request):
     )  # Shorten it a bit, bytes wise
 
     def __init__(self, *args, **kwargs):
+        self.client_addr = None
+        self.server_addr = None
         try:
             http.Request.__init__(self, *args, **kwargs)
             # Easy server link
@@ -77,9 +79,6 @@ class WebRequest(http.Request):
                 # requires unicode string.
                 self.client_addr = [str(self.client.host), self.client.port]
                 self.server_addr = [str(self.host.host), self.host.port]
-            else:
-                self.client_addr = None
-                self.server_addr = None
 
             self.client_scheme = "https" if self.isSecure() else "http"
 


Test patch:
diff --git a/tests/test_http_protocol.py b/tests/test_http_protocol.py
new file mode 100644
index 0000000..024479d
--- /dev/null
+++ b/tests/test_http_protocol.py
@@ -0,0 +1,49 @@
+import unittest
+
+from daphne.http_protocol import WebRequest
+
+
+class MockServer:
+    """
+    Mock server object for testing.
+    """
+
+    def protocol_connected(self, *args, **kwargs):
+        pass
+
+
+class MockFactory:
+    """
+    Mock factory object for testing.
+    """
+
+    def __init__(self):
+        self.server = MockServer()
+
+
+class MockChannel:
+    """
+    Mock channel object for testing.
+    """
+
+    def __init__(self):
+        self.factory = MockFactory()
+        self.transport = None
+
+    def getPeer(self, *args, **kwargs):
+        return "peer"
+
+    def getHost(self, *args, **kwargs):
+        return "host"
+
+
+class TestHTTPProtocol(unittest.TestCase):
+    """
+    Tests the HTTP protocol classes.
+    """
+
+    def test_web_request_initialisation(self):
+        channel = MockChannel()
+        request = WebRequest(channel)
+        self.assertIsNone(request.client_addr)
+        self.assertIsNone(request.server_addr)