From cc2910afa9afa900591e4f061e9bd0ecd1d56380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 9 Oct 2026 11:23:24 +0200 Subject: [PATCH] fix cache --- CACHE_SETUP.md | 14 ++++++++++++++ adlist/converter.py | 4 ++++ tests/smoke_routes.py | 5 +++++ 3 files changed, 23 insertions(+) diff --git a/CACHE_SETUP.md b/CACHE_SETUP.md index f339728..2db9a4c 100644 --- a/CACHE_SETUP.md +++ b/CACHE_SETUP.md @@ -76,3 +76,17 @@ Run local code checks: python3 -m unittest discover -s tests -p 'test_*.py' -v python3 tests/smoke_cache_routes.py ``` + +## Duplicate Date and RustProxy 0.29.3 + +`/convert` deliberately does not set its own `Date` header after +`Response.make_conditional()`. Werkzeug can create a `Date` header there; +Uvicorn also supplies one, and RustProxy 0.29.3 rejects multiple `Date` +headers with `response_ttl_size_or_metadata`. + +Verify that **exactly one** `Date` header is returned directly by the +ASGI backend (no reverse proxy in the path). All success responses should +also include a valid `Content-Length`, `ETag` and public `Cache-Control`. +For a full seven days, set RustProxy global `max_ttl_seconds` to `604800` +AND configure the app's `CACHE_S_MAXAGE=604800` in `.env`. +The `unsafe_force` cache switch is not required for `/convert`. diff --git a/adlist/converter.py b/adlist/converter.py index 46f9f99..953d0ac 100644 --- a/adlist/converter.py +++ b/adlist/converter.py @@ -162,6 +162,10 @@ def convert(): # the final representation. It may return 304 only after validating # the complete content hash. Never use the source's validators. resp.make_conditional(request) + # Werkzeug adds Date during make_conditional(), while Uvicorn adds + # its own Date. Two Date fields make RustProxy 0.29.3 reject caching. + # Leave Date generation to the HTTP server, including for HTTP 304. + resp.headers.pop("Date", None) if resp.status_code == 304: result.output.close() redis_client.incr("stats:conversions_success") diff --git a/tests/smoke_routes.py b/tests/smoke_routes.py index 01ab18b..1c2edd3 100644 --- a/tests/smoke_routes.py +++ b/tests/smoke_routes.py @@ -34,6 +34,8 @@ class Response: self.close_callbacks.append(callback) def make_conditional(self, request): + # Simulate Werkzeug Response.make_conditional adding Date. + self.headers["Date"] = "Fri, 09 Oct 2026 08:58:26 GMT" if request.headers.get("If-None-Match") == self.headers.get("ETag"): self.status_code = 304 return self @@ -196,6 +198,7 @@ check(resp.headers["Cache-Control"] == "proxy TTL and staleness") check(resp.headers["ETag"] == f'"{content_hash}"', "strong output-body ETag") check(resp.headers["Content-Length"] == str(len(converted)), "content length") +check("Date" not in resp.headers, "GET leaves Date to Uvicorn (no duplicate)") check(b"".join(resp.data) == converted, "response bytes") check(source.closed, "upstream closed after success") check("Last-Modified" not in resp.headers, "origin validator is not forwarded") @@ -207,6 +210,7 @@ check(resp.status_code == 304, "client 304 after full body hash") check("headers" not in called[-1][1], "client validators not forwarded to upstream") check(source.closed, "conditional upstream closed") check(resp.headers["ETag"] == f'"{content_hash}"', "304 keeps content validator") +check("Date" not in resp.headers, "304 leaves Date to Uvicorn (no duplicate)") source = FakeUpstream(b"0.0.0.0 another.test\n", headers={"ETag": '"upstream-pretend"'}) changed = run() @@ -225,6 +229,7 @@ check(head.status_code == 200, "HEAD status") check(head.data is None, "HEAD no data iterator") check(head.headers["ETag"] == f'"{content_hash}"', "GET/HEAD same ETag") check(head.headers["Content-Length"] == str(len(converted)), "HEAD real content length") +check("Date" not in head.headers, "HEAD leaves Date to Uvicorn (no duplicate)") source = FakeUpstream(converted) other = run(args={"url": "https://source.test/list.txt", "ip": "127.0.0.1"})