Skip to content

Defer the session cookie until session data is written so anonymous page views stay cacheable#3211

Merged
brianhogg merged 2 commits into
gocodebox:devfrom
lifterlms-maurice:fix/anonymous-session-cookie-page-cache
Jun 22, 2026
Merged

Defer the session cookie until session data is written so anonymous page views stay cacheable#3211
brianhogg merged 2 commits into
gocodebox:devfrom
lifterlms-maurice:fix/anonymous-session-cookie-page-cache

Conversation

@lifterlms-maurice

@lifterlms-maurice lifterlms-maurice commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Description

LifterLMS emits the wp_llms_session_<COOKIEHASH> cookie on the first page view of every anonymous visitor, even on pages that never touch the session. Full-page caches won't store responses carrying a Set-Cookie header, so this disables anonymous page caching site-wide whenever LifterLMS is active.

This makes the session cookie lazy. A brand-new session lives in memory only (no Set-Cookie, no DB row) until the first write via LLMS_Session::set() (an applied coupon, a queued notice, etc.), at which point the cookie is emitted. Anonymous visitors who never write session data never receive the cookie, so those responses stay cache-eligible. Requests that do write to the session (checkout, coupon AJAX, notice-queuing) still set the cookie and correctly bypass the cache.

The cookie is emitted from set() the same way it was previously emitted from the constructor: unconditionally, during request processing before any output. Every first-party write path (core checkout + AJAX coupons, llms_add_notice(), Advanced Coupons coupon-from-URL, Gifts voucher count, Groups seat count) runs before output, so the cookie is emitted in time to take effect.

Mirrors the approach already taken for the llms-tracking cookie in #2330 / #2331. Default-on, with the existing llms_session_should_init filter left intact as an escape hatch. Verified against core and all first-party add-ons that every session writer goes through LLMS_Session::set() and none relies on eager cookie creation.

Fixes #3210

How has this been tested?

Ran the PHPUnit suite locally against this branch:

composer run-script tests-run -- --group sessions,coupons
OK (65 tests, 452 assertions)

Updated test_init_cookie_new() to assert a new session emits no cookie until data is written, then emits on first set(). The existing-cookie tests (test_get_cookie, test_init_cookie_from_existing_expiring, test_init_cookie_from_existing_user_logged_in) previously relied on the constructor eagerly creating a cookie; they now seed one explicitly to match the lazy behavior. The coupon AJAX group exercises the real session-write path and passes.

Screenshots

N/A.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • Performance improvement

Checklist:

  • My code has been tested.
  • My code follows the LifterLMS Coding & Documentation Standards.
  • Source-only changes; no compiled assets committed.
  • Changelog entry added (.changelogs/anonymous-session-cookie-page-cache.yml).

LifterLMS emitted the wp_llms_session_<COOKIEHASH> cookie on the first
page view of every anonymous visitor, even on pages that never touch the
session. Full-page caches won't store responses carrying a Set-Cookie
header, so this disabled anonymous page caching site-wide whenever
LifterLMS was active.

Make the session cookie lazy: a brand-new session lives in memory only
(no Set-Cookie, no DB row) until the first write via LLMS_Session::set(),
at which point the cookie is emitted. Anonymous visitors who never write
session data never receive the cookie, so those responses stay
cache-eligible. Mirrors the llms-tracking cookie approach from gocodebox#2330 / gocodebox#2331.

Fixes gocodebox#3210

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brianhogg brianhogg moved this to Awaiting Review in Development Jun 20, 2026
The headers_sent() guard around the deferred set_cookie() call dropped the
cookie whenever output had already started, which broke the new behavior
under PHPUnit (and would have failed in CI). The eager cookie was
previously emitted from the constructor with no such guard, and every
first-party session write runs before output, so emit unconditionally to
match that behavior and keep it testable.

Seed a cookie in the existing-cookie session tests that previously relied
on the constructor eagerly creating one.

Verified locally: tests-run --group sessions,coupons -> 65 tests, 452 assertions, OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brianhogg brianhogg merged commit a71cc7c into gocodebox:dev Jun 22, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from Awaiting Review to Done in Development Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants