Repository: encode/uvicorn

Issue:
"Date" header not changing between requests
### Discussed in https://github.com/encode/uvicorn/discussions/1610

<div type='discussions-op-text'>

<sup>Originally posted by **arjwilliams** August 22, 2022</sup>
I am testing out some cache-control headers in my web app, but the auto-generated "Date" header seems to only be changing when a new connection is established, rather than with each new request, which is preventing the browser from caching new responses, as it sees the response as being older than it actually is.

I would expect that the "Date" header to change between requests, even when using an existing connection.

I am testing on Python 3.10.5, with Uvicorn 0.18.2.

Possible test case, which is currently failing for me:
```python
import asyncio

from email.utils import parsedate_to_datetime
from contextlib import asynccontextmanager

import httpx

from uvicorn import Config, Server

async def test_default_date_header_different():
    config = Config(app=app, loop="asyncio", limit_max_requests=2)
    async with run_server(config):
        async with httpx.AsyncClient() as client:
            response1 = await client.get("http://127.0.0.1:8000")
            await asyncio.sleep(2)
            response2 = await client.get("http://127.0.0.1:8000")

            response1_date = parsedate_to_datetime(response1.headers["date"])
            response2_date = parsedate_to_datetime(response2.headers["date"])
            assert(response2_date > response1_date)

async def app(scope, receive, send):
    assert scope["type"] == "http"
    await send({"type": "http.response.start", "status": 200, "headers": []})
    await send({"type": "http.response.body", "body": b"", "more_body": False})

@asynccontextmanager
async def run_server(config: Config):
    server = Server(config=config)
    cancel_handle = asyncio.ensure_future(server.serve())
    await asyncio.sleep(0.1)
    try:
        yield server
    finally:
        await server.shutdown()
        cancel_handle.cancel()

asyncio.run(test_date_header_different())
```</div>


Discussion:
not sure about that, isn't that the keep-alive of 5s is for, the tests pass fine if for instance we set 
```
    config = Config(app=app, timeout_keep_alive=1.1)
    server = Server(config=config)
```
@euri10 that would mean the connection expires on TimeOut right?

The discussion here was mainly focusing on repeated request using same connection.

The discussion has a rough comparison with other servers too. 
> not sure about that, isn't that the keep-alive of 5s is for, the tests pass fine if for instance we set
> 
> ```
>     config = Config(app=app, timeout_keep_alive=1.1)
>     server = Server(config=config)
> ```

Correct. This is not a bug.
After reading around a bit, I think this is a bug. 😅

If the same connection is kept alive for a year, it will still show the same date. I don't think that's expected...

EDIT: I also don't think that a connection would be alive for a year, fwiw. 👀
One year!?😅

For a possible fix, we may take reference from Hypercorn and set default headers on every response. If we do this, we might not need the on_tick function to update default header on every second.

Resolving patch:
diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py
--- a/uvicorn/protocols/http/h11_impl.py
+++ b/uvicorn/protocols/http/h11_impl.py
@@ -92,7 +92,6 @@ def __init__(
         self.server_state = server_state
         self.connections = server_state.connections
         self.tasks = server_state.tasks
-        self.default_headers = server_state.default_headers
 
         # Per-connection state
         self.transport: asyncio.Transport = None  # type: ignore[assignment]
@@ -230,7 +229,7 @@ def handle_events(self) -> None:
                     logger=self.logger,
                     access_logger=self.access_logger,
                     access_log=self.access_log,
-                    default_headers=self.default_headers,
+                    default_headers=self.server_state.default_headers,
                     message_event=asyncio.Event(),
                     on_response=self.on_response_complete,
                 )
diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py
--- a/uvicorn/protocols/http/httptools_impl.py
+++ b/uvicorn/protocols/http/httptools_impl.py
@@ -90,7 +90,6 @@ def __init__(
         self.server_state = server_state
         self.connections = server_state.connections
         self.tasks = server_state.tasks
-        self.default_headers = server_state.default_headers
 
         # Per-connection state
         self.transport: asyncio.Transport = None  # type: ignore[assignment]
@@ -199,7 +198,7 @@ def handle_upgrade(self) -> None:
     def send_400_response(self, msg: str) -> None:
 
         content = [STATUS_LINE[400]]
-        for name, value in self.default_headers:
+        for name, value in self.server_state.default_headers:
             content.extend([name, b": ", value, b"\r\n"])
         content.extend(
             [
@@ -274,7 +273,7 @@ def on_headers_complete(self) -> None:
             logger=self.logger,
             access_logger=self.access_logger,
             access_log=self.access_log,
-            default_headers=self.default_headers,
+            default_headers=self.server_state.default_headers,
             message_event=asyncio.Event(),
             expect_100_continue=self.expect_100_continue,
             keep_alive=http_version != "1.0",


Test patch:
diff --git a/tests/test_default_headers.py b/tests/test_default_headers.py
--- a/tests/test_default_headers.py
+++ b/tests/test_default_headers.py
@@ -1,9 +1,16 @@
+import asyncio
+import datetime as dt
+
+import anyio
+import anyio.abc
 import httpx
 import pytest
 
 from tests.utils import run_server
 from uvicorn import Config
 
+DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
+
 
 async def app(scope, receive, send):
     assert scope["type"] == "http"
@@ -20,6 +27,41 @@ async def test_default_default_headers():
             assert response.headers["server"] == "uvicorn" and response.headers["date"]
 
 
+@pytest.mark.anyio
+async def test_date_headers_update():
+    config = Config(app=app, loop="asyncio")
+    async with run_server(config):
+        async with httpx.AsyncClient() as client:
+            response = await client.get("http://127.0.0.1:8000")
+            date = response.headers["date"]
+            first_date = dt.datetime.strptime(date, DATE_FORMAT)
+            second_date = first_date
+
+            cancelled = False
+
+            async def sleep_and_cancel(tg: anyio.abc.TaskGroup):
+                nonlocal cancelled
+                await asyncio.sleep(2)
+                cancelled = True  # pragma: no cover
+                await tg.cancel_scope.cancel()  # pragma: no cover
+
+            async def ensure_different_date(tg: anyio.abc.TaskGroup):
+                nonlocal second_date
+                async with httpx.AsyncClient() as client:
+                    while second_date == first_date:
+                        response = await client.get("http://127.0.0.1:8000")
+                        date = response.headers["date"]
+                        second_date = dt.datetime.strptime(date, DATE_FORMAT)
+                tg.cancel_scope.cancel()
+
+            async with anyio.create_task_group() as tg:
+                tg.start_soon(sleep_and_cancel, tg)
+                tg.start_soon(ensure_different_date, tg)
+
+            assert not cancelled
+            assert second_date - first_date == dt.timedelta(seconds=1)
+
+
 @pytest.mark.anyio
 async def test_override_server_header():
     config = Config(