dotCMS / dotCMS/core

Reduce unnecessary JSESSIONID session cookies on anonymous/public page requests

Open
#36,094 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OKR : Application Performance Priority : 3 Average stale
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

This needs a sanity check. The goal is to behave and perform better behind CDNs.

Summary

Several code paths in the filter chain and visitor tracking system create HTTP sessions (and thus send JSESSIONID cookies) for every anonymous page request, even when no session state is needed. This increases memory pressure on the session store (especially with Redis session replication), inflates cookie headers on every response, and undermines cacheability of public pages.

Cookies involved

Cookie Type Set by Issue
JSESSIONID Tomcat session cookie Tomcat container, triggered by any request.getSession() call Created for every anonymous page view by multiple code paths (findings 1–5 below)
opvc dotCMS session cookie NumberOfTimeVisitedCounterresponse.addCookie() Set on every first page view per browser session, no opt-out (finding 6)
sitevisitscookie dotCMS persistent cookie (5 years) NumberOfTimeVisitedCounterresponse.addCookie() Set/updated on every first page view, no opt-out (finding 6)

Note: The dmid (long-lived visitor ID) cookie and rme (JWT remember-me) cookie are not affected — dmid is created in CookieUtil.createCookie() but the call in ClickstreamFactory.addRequest() (line 156) discards the return value without adding it to the response, and rme is only set on explicit "remember me" login.

Problem

A typical anonymous page view currently triggers JSESSIONID session cookie creation from multiple independent code paths, and additionally receives opvc + sitevisitscookie tracking cookies:

1. CMSFilter.countPageVisit()VisitorAPIImpl.getVisitor(request)highest impact

Files: CMSFilter.java:235, VisitorAPIImpl.java:52,70

CMSFilter.countPageVisit() calls visitorAPI.getVisitor(request) with no create argument. This defaults to CREATE_VISITOR_OBJECT_IN_SESSION=true, which calls request.getSession() — creating a new session and JSESSIONID for every anonymous visitor.

// CMSFilter.java:235
Optional<Visitor> visitor = visitorAPI.getVisitor(request); // defaults create=true

// VisitorAPIImpl.java:70
final HttpSession session = request.getSession(); // creates session unconditionally
2. BaseCharacter visitor logging creates sessions for analytics

File: BaseCharacter.java:70-71

The visitor logging system (runs when license >= STANDARD) calls request.getSession().getId() just to log the session ID — creating a session as a side effect.

myMap.get().put("session", request.getSession().getId());       // line 70
myMap.get().put("sessionNew", request.getSession().isNew());    // line 71
3. RulesEngineCharacter and GDPRCharacter use getSession() without false

Files: RulesEngineCharacter.java:23-24, GDPRCharacter.java:30-31,59-60

Both call request.getSession() to check for session attributes, creating sessions even when they just need to read (and could safely get null).

// RulesEngineCharacter.java:23
if (request.getSession().getAttribute(WebKeys.RULES_ENGINE_FIRE_LIST) != null) {

// GDPRCharacter.java:30
String user = (request.getSession().getAttribute(WebKeys.CMS_USER) != null)
4. ClickstreamFactory.addRequest() forces session creation

File: ClickstreamFactory.java:74-76

HttpSession session = request.getSession();
Clickstream clickstream = (Clickstream) request.getSession(true).getAttribute("clickstream");

Creates a session and stores clickstream data in it. While ClickstreamFilter itself is commented out/deprecated, ClickstreamFactory.addRequest() may still be invoked from other paths.

5. LanguageWebAPIImpl creates sessions on language change

File: LanguageWebAPIImpl.java:134

When a language_id parameter is present and differs from the current language, a session is created to persist the language preference — even for anonymous visitors.

sessionOpt = httpRequest.getSession(true);  // creates session on language change
6. Tracking cookies set on every first page view

Files: NumberOfTimeVisitedCounter.java, CookieUtil.java

CMSFilter.countSiteVisit() calls NumberOfTimeVisitedCounter.maybeCount() for all LIVE page requests, which sets:

  • opvc — session cookie (once-per-visit marker)
  • sitevisitscookie — 5-year persistent cookie (visit counter)

These are unconditionally added for every new anonymous visitor with no opt-out mechanism.

Code paths doing it correctly (for reference)

  • CharsetEncodingFilter — uses getSession(false)
  • TimeMachineFilter — checks getSession(false) before proceeding ✅
  • HealthProbeServlet — no session creation ✅
  • VisitorAPIImpl.getVisitor(request, false) — correctly uses getSession(false) ✅ (but nobody calls it with false from the filter chain)

Suggested Resolution

A. Guard session creation in CMSFilter.countPageVisit()

Change to use getVisitor(request, false) so a Visitor/session is not created if one doesn't already exist:

private void countPageVisit(HttpServletRequest request) {
    PageMode mode = PageMode.get(request);
    if (mode == PageMode.LIVE) {
        Optional<Visitor> visitor = visitorAPI.getVisitor(request, false);
        visitor.ifPresent(v -> v.addPagesViewed(request.getRequestURI()));
    }
}

Alternatively, make CREATE_VISITOR_OBJECT_IN_SESSION default to false and let deployments that need visitor tracking opt in.

B. Fix BaseCharacter to use getSession(false)
HttpSession session = request.getSession(false);
myMap.get().put("session", session != null ? session.getId() : "none");
myMap.get().put("sessionNew", session != null && session.isNew());
C. Fix RulesEngineCharacter and GDPRCharacter

Replace all request.getSession().getAttribute(...) calls with null-safe getSession(false) patterns:

HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(WebKeys.RULES_ENGINE_FIRE_LIST) != null) {
    // ...
}
D. Make visitor tracking cookies configurable

Add a config property (e.g., ENABLE_VISITOR_TRACKING_COOKIES=true) to gate NumberOfTimeVisitedCounter.maybeCount(), allowing deployments to disable anonymous tracking cookies entirely.

E. Guard LanguageWebAPIImpl session creation

Store language preference in a cookie instead of creating a session, or only persist to session if one already exists.

Impact

  • Reduces session store memory usage for sites with high anonymous traffic
  • Eliminates unnecessary Set-Cookie: JSESSIONID headers on public page responses
  • Improves CDN/proxy cacheability of anonymous page responses
  • Reduces tracking cookie exposure for GDPR/privacy compliance

Environment

All versions — these patterns have been present for a long time.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with CMSFilter.countPageVisit() and trace the session behavior through VisitorAPIImpl.java, BaseCharacter.java, RulesEngineCharacter.java, GDPRCharacter.java, ClickstreamFactory.java, LanguageWebAPIImpl.java, NumberOfTimeVisitedCounter.java, and CookieUtil.java. Review the existing getSession(false) paths and determine the intended scope before choosing among the suggested resolutions. Done means the selected anonymous request paths no longer create unnecessary sessions or tracking cookies, with behavior verified across the affected entry points.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.