Session Lifecycle and State Tracking
The session system in this project relies on a state-tracking mechanism to determine when data has been read, when it has been changed, and how those changes should be reflected in the HTTP response. This is primarily managed through the SessionMixin and its implementation in SecureCookieSession.
State Tracking Flags
The SessionMixin class in src/flask/sessions.py defines four key attributes that track the lifecycle of a session during a single request-response cycle.
Accessed
The accessed flag indicates whether the session was read during the request. This is not just for manual lookups; any interaction with the session proxy sets this to True.
In src/flask/ctx.py, the RequestContext ensures this flag is set whenever the session is retrieved:
@property
def session(self) -> SessionMixin:
# ...
session = self._get_session()
session.accessed = True
return session
When accessed is True, the SecureCookieSessionInterface adds a Vary: Cookie header to the response. This informs downstream caches that the response content may vary based on the user's session cookie.
Modified
The modified flag determines if the session data should be written back to the client. The SecureCookieSession class (in src/flask/sessions.py) automates this by inheriting from CallbackDict. It defines an on_update callback that triggers whenever a key is set or deleted:
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
modified = False
def __init__(self, initial: c.Mapping[str, t.Any] | None = None) -> None:
def on_update(self: te.Self) -> None:
self.modified = True
super().__init__(initial, on_update)
The Nested Mutation Gotcha: Because modified is only triggered by the dictionary's top-level __setitem__ and __delitem__ methods, modifying a mutable object stored inside the session (like a list) will not automatically set modified to True. In such cases, you must set it manually:
# This will NOT trigger a save automatically
session["my_list"].append(1)
# You must do this:
session.modified = True
Permanent
The permanent flag controls the expiration behavior of the session cookie. It is backed by the _permanent key within the session dictionary.
- If
False(default): The cookie is a "session cookie" and expires when the browser is closed. - If
True: The cookie is given an expiration date based on thePERMANENT_SESSION_LIFETIMEconfiguration (defaulting to 31 days).
New
The new flag is intended to indicate if the session was created during the current request. However, in the default SecureCookieSession implementation, it is hard-coded to False because the backend cannot reliably distinguish between a brand-new session and one that was simply empty.
The Session Lifecycle
The session lifecycle is managed by the SessionInterface through two primary phases: open_session and save_session.
1. Opening the Session
At the start of a request, RequestContext.push() calls open_session. The SecureCookieSessionInterface reads the session cookie, verifies its signature using itsdangerous, and populates a SecureCookieSession object. If the cookie is missing or the signature is invalid, a new, empty session object is created.
2. Saving the Session
At the end of the request, save_session is called. This method uses should_set_cookie to decide if a Set-Cookie header is necessary.
The logic in SessionInterface.should_set_cookie is:
def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
return session.modified or (
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
)
If SESSION_REFRESH_EACH_REQUEST is enabled (the default), permanent sessions will have their cookie refreshed on every request even if the data hasn't changed, ensuring the expiration date "slides" forward.
Impact on the Response
The SecureCookieSessionInterface.save_session method in src/flask/sessions.py performs the final synchronization with the HTTP response:
- Vary Header: If
session.accessedisTrue, it addsVary: Cookie. - Deletion: If the session is empty but was
modified(e.g., viasession.clear()), it sends a cookie deletion header. - Persistence: If
should_set_cookiereturnsTrue, it serializes the session data, signs it, and adds theSet-Cookieheader with the appropriateexpires,httponly, andsecureflags.
# Example of how internal functions like flash() affect state
def test_flashes(app, req_ctx):
assert not flask.session.modified
flask.flash("Hello")
# flash() modifies the internal '_flashes' list,
# which triggers the CallbackDict to set modified = True
assert flask.session.modified
This state-tracking ensures that cookies are only sent when necessary, minimizing header overhead while maintaining security and session persistence.