Session depersonalization in Magento 2, why and how?

Most likely, at least once you faced the issue when customer session does not work except an account page etc. And usually, it’s not obvious why. Is it a bug? No, actually that is the case when it’s not a bug, it’s kind of feature.

Public and private content

Magento can distinguish between two types of content:

  • Public – public content is stored server-side in your reverse proxy cache storage;
  • Private – private content is stored client side and is specific to an individual customer.

There are the cacheable and non-cacheable pages. It’s pretty simple with non-cacheable pages. Such pages are not cacheable and can provide dynamically generated private content, unique for one user. The cacheable pages are being served by reverse proxies to more than one user. And it’s very important to avoid caching of the user-specific data on these pages.

Here is the cacheable page checklist:

  • The HTTP GET or HTTP HEAD request is used;
  • Pages render only cacheable blocks (there are no cacheable="false" attribute for any block in layout);
  • Pages render without sensitive private data, session and customer DTO objects are empty;
  • Model and block level should identify themselves for invalidation support.

Customer Session Depersonalization

Magento cleans the session storage for cacheable requests in order to avoid caching of customer private content. This indicates that we should not access the customer session data while processing the GET request intended to render the cacheable page. For example, when rendering the product view page, which is cacheable by default, if you try to check whether the customer is logged in, you will always receive false.

The responsibility to unset the private data lies on several depersonalization Magento 2 plugin classes. These plugins are applied to Magento\Customer\Model\Layout\DepersonalizePlugin::beforeGenerateXml method, as this is the most suitable entry point, considering all the necessary data is already available and the rendering process has not started yet. These depersonalized plugins implement two methods:

  • beforeGenerateXml – collects all the needed data, which should be kept for the further usage;
  • afterGenerateXml – unset the private customer data from the session DTO, set the specific previously saved data, which should be kept.

There is an additional “Depersonalized Checker” class which is intended to check if the session should be depersonalized. This class implements only one method, which is expected to be used for all depersonalized Magento 2 plugins. If you check the code you will see that in general, it performs the verification of “cacheable page checklist” described previously.

/**
 * Check if depersonalize or not
 *
 * @param \Magento\Framework\View\LayoutInterface $subject
 * @return bool
 * @api
 */
public function checkIfDepersonalize(\Magento\Framework\View\LayoutInterface $subject)
{
    return ($this->moduleManager->isEnabled('Magento_PageCache')
        && $this->cacheConfig->isEnabled()
        && !$this->request->isAjax()
        && ($this->request->isGet() || $this->request->isHead())
        && $subject->isCacheable());
}

Here is what it does:

  • Check if the FPC (Full Page Cache) module is enabled;
  • Check if the FPC cache type is enabled;
  • Check if the request is not AJAX;
  • Check if the HTTP GET or HTTP HEAD method is used for the request;
  • Check if the page is cacheable, by verifying if non-cacheable layout elements (blocks with cacheable="false" attribute) are existent on the page.

If all of the above conditions are true, the page is considered as cacheable.

The customer session is cleaned by \Magento\Customer\Model\Layout\DepersonalizePlugin. Let’s check the implementation of Magento 2 before plugin method beforeGenerateXml. It simply puts the necessary session data to the corresponding properties of the class, in order to keep this data after session storage cleanup.

/**
 * Before generate Xml
 *
 * @param \Magento\Framework\View\LayoutInterface $subject
 * @return array
 */
public function beforeGenerateXml(\Magento\Framework\View\LayoutInterface $subject)
{
    if ($this->depersonalizeChecker->checkIfDepersonalize($subject)) {
        $this->customerGroupId = $this->customerSession->getCustomerGroupId();
        $this->formKey = $this->session->getData(\Magento\Framework\Data\Form\FormKey::FORM_KEY);
    }
    return [];
}

As for the Magento 2 after plugin method afterGenerateXml, it unsets the visitor’s data and clears session storage. So the customer private data is not related to the current visitor and is not accessible anymore. Then it sets the previously saved data (customer group id and form key) as an exclusion for specific needs.

/**
 * After generate Xml
 *
 * @param \Magento\Framework\View\LayoutInterface $subject
 * @param \Magento\Framework\View\LayoutInterface $result
 * @return \Magento\Framework\View\LayoutInterface
 */
public function afterGenerateXml(\Magento\Framework\View\LayoutInterface $subject, $result)
{
    if ($this->depersonalizeChecker->checkIfDepersonalize($subject)) {
        $this->visitor->setSkipRequestLogging(true);
        $this->visitor->unsetData();
        $this->session->clearStorage();
        $this->customerSession->clearStorage();
        $this->session->setData(\Magento\Framework\Data\Form\FormKey::FORM_KEY, $this->formKey);
        $this->customerSession->setCustomerGroupId($this->customerGroupId);
        $this->customerSession->setCustomer($this->customerFactory->create()->setGroupId($this->customerGroupId));
    }
    return $result;
}

There are many other depersonalization plugins. Most of them are also used in order to clear particular session storage (e.g. Magento checkout session, Magento catalog session) or vice versa – to keep some specific data in session DTO:

Name Class
catalog-session-depersonalize Magento\Catalog\Model\Layout\DepersonalizePlugin
checkout-session-depersonalize Magento\Checkout\Model\Layout\DepersonalizePlugin
core-session-depersonalize Magento\PageCache\Model\Layout\DepersonalizePlugin
persistent-session-depersonalize Magento\Persistent\Model\Layout\DepersonalizePlugin
tax-session-depersonalize Magento\Tax\Model\Layout\DepersonalizePlugin
customer-session-depersonalize Magento\Customer\Model\Layout\DepersonalizePlugin

Possible Solutions

So how to deal with customer private content for cacheable pages?

Someone recommends disabling Full Page Caching. However, this is not an option. You should never do it. This is critically important to avoid such solutions while implementing a custom feature.

The second one is to make the cacheable page to be non cacheable by using the cacheable="false" attribute for your block in layout. Which is good when your page consists of dynamic and private content entirely e.g. Shopping Cart, Account View, Order History pages. However, you should be careful when using this approach. If you add the cacheable="false" attribute to some block – all the pages where it’s used will be uncacheable. For example, if you add such block to the default handle of your layout update, you will make the whole website non-cacheable by full page cache, none of the pages will be cached.

And the highly recommended approach is a separate AJAX request performed in order to load the private content and update dynamic blocks e.g. minicart, wishlist (in sidebar), customer name (in header). Magento 2 provides a special JS component to simplify the work with a customer private content. More details can be found in the Private Content section of Magento 2 DevDocs.

Hope this information was interesting for you too. Thanks for reading!