From f834f41a037e16c98d5f6504eb061abba6f92f9f Mon Sep 17 00:00:00 2001 From: Zhaoqi Xu Date: Thu, 1 Oct 2026 18:06:06 +0800 Subject: [PATCH] fix: do not mark session accessed in process_response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading ctx.session sets session.accessed, which made SecureCookieSessionInterface.save_session add Vary: Cookie on every response — including views that never touched the session. Use ctx._session instead, matching Flask's ctx._get_session() pattern. Fixes #480 --- src/quart/app.py | 15 +++++++++++---- tests/test_sessions.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/quart/app.py b/src/quart/app.py index bb2e42c6..2e2b8834 100644 --- a/src/quart/app.py +++ b/src/quart/app.py @@ -1690,8 +1690,13 @@ async def process_response( ResponseTypes, await self.ensure_async(function)(response) ) - session_ = ctx.session - if not self.session_interface.is_null_session(session_): + # Use _session, not the session property: the property sets + # session.accessed = True, which would force Vary: Cookie on every + # response (including views that never touched the session). + session_ = ctx._session + if session_ is not None and not self.session_interface.is_null_session( + session_ + ): await self.ensure_async(self.session_interface.save_session)( self, session_, response ) @@ -1720,8 +1725,10 @@ async def postprocess_websocket( ResponseTypes, await self.ensure_async(function)(response) ) - session_ = ctx.session - if not self.session_interface.is_null_session(session_): + session_ = ctx._session + if session_ is not None and not self.session_interface.is_null_session( + session_ + ): await self.session_interface.save_session(self, session_, response) return response diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 130b0e6b..08ab3adb 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -5,6 +5,7 @@ from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers +from quart import session from quart.app import Quart from quart.sessions import SecureCookieSession from quart.sessions import SecureCookieSessionInterface @@ -84,3 +85,25 @@ async def test_secure_cookie_session_interface_save_session_no_access() -> None: response = await _save_session(session) assert response.headers.get("Set-Cookie") is None assert response.headers.get("Vary") is None + + +async def test_process_response_does_not_mark_session_accessed() -> None: + """Views that never touch the session must not get Vary: Cookie (#480).""" + app = Quart(__name__) + app.secret_key = "secret" + + @app.route("/") + async def index() -> str: + return "ok" + + @app.route("/touch") + async def touch() -> str: + session["x"] = 1 + return "ok" + + test_client = app.test_client() + response = await test_client.get("/") + assert response.headers.get("Vary") is None + + response = await test_client.get("/touch") + assert response.headers.get("Vary") == "Cookie"