Fixing Liferay GA132 Bug: Client Extensions Being Removed After Modification

By Rafał Pydyniak | 2025-05-14

The Problem: Disappearing Client Extensions in Liferay GA132

If you've recently upgraded to Liferay GA132 and noticed that your Client Extensions (CX) keep disappearing from pages after modification, you're not alone. I've encountered this frustrating issue and traced it back to a specific problem in the platform's code.

The issue originates in the CETConfigurationFactory.java, which contains code that removes Client Extension entries from pages after each deactivation and modification.

The problematic fragment:

_clientExtensionEntryRelLocalService.
    deleteClientExtensionEntryRels(
        companyId, _cet.getExternalReferenceCode());

This code is triggered when a Client Extension is modified or deactivated, causing all relations to pages to be removed. While this issue definitely affects Global JS Client Extensions, it likely impacts other types as well, such as Global CSS and potentially others.

When looking at the codebase, it appears this behavior was introduced in GA132, though it's possible it could exist in earlier versions that I haven't tested.

The Impact: Development Frustration and Production Risks

For developers, this bug creates a significant productivity issue. Every time you modify and deploy your Client Extension, you'll need to manually re-add it to all the pages where it was previously used. In a development environment where frequent changes are the norm, this becomes an enormous time sink.

More concerning is the potential impact on production environments. If a developer or administrator modifies a Client Extension in production and forgets to re-add it to all the necessary pages, this could result in broken functionality for end users. Since there's no warning or notification about the removed relations, this type of error could easily go unnoticed until reported by users - creating a poor user experience and potentially affecting business operations.

The most frustrating part is that there's no built-in configuration option to disable this behavior. Looking at the code structure, a custom implementation is necessary to solve the problem.

Official Bug Status

I did some research and I've discovered that this issue has already been reported in the Liferay issue tracking system as LPD-50051, and a fix has been implemented in a pull request.

However, there's an important consideration for Community Edition (CE) users: since CE doesn't receive regular patches, it could be months before this fix makes its way into the next official CE release. If you're using CE and encountering this issue, you'll likely need to implement the workaround described in this article until an updated version becomes available.

Enterprise subscribers may receive the fix sooner through regular patches, but even then, the timeline for patch delivery can vary. The custom implementation described here provides an immediate solution while waiting for the official fix.

Solutions: Creating a Custom Implementation

If you, just like me, cannot wait for next version then you need to figure out a solution.

Option 1: Create a Custom ServiceWrapper

You could create a custom clientExtensionEntryRelLocalServiceWrapper that checks if the delete request is coming from the CETConfigurationFactory class and, if so, prevents the deletion.

While this approach would work, it requires more complex logic to identify the origin of the delete request, making it potentially fragile if internal implementations change.

A cleaner approach is to provide a custom implementation of the CETConfigurationFactory itself, where we remove the problematic code, and disable the original factory.

This is the approach I recommend and will detail below.

Implementation Guide: Replacing the CETConfigurationFactory

Here's how to implement the solution:

Step 1: Create a Custom CETConfigurationFactory

Create a custom implementation of CETConfigurationFactory where you remove the problematic code. Essentially, this will be a copy of the original class with the offending lines removed or commented out.

The modified version of the modified method would look like this:

@Modified
protected void modified(Map<String, Object> properties) throws Exception {
   _properties = properties;

   String externalReferenceCode = _getExternalReferenceCode(properties);

   if (_log.isDebugEnabled()) {
      _log.debug(
         StringBundler.concat(
            "Modifying client extension ", externalReferenceCode,
            "with properties:\n", MapUtil.toString(properties)));
   }
   else if (_log.isInfoEnabled()) {
      _log.info("Modifying client extension " + externalReferenceCode);
   }

   ConfigurationFactoryUtil.executeAsCompany(
      _companyLocalService, properties,
      companyId -> {
         try {
            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     "Deleting CET for client extension ",
                     externalReferenceCode, " and company ",
                     companyId));
            }

            _cetManager.deleteCET(_cet);

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     "Adding CET for client extension ",
                     externalReferenceCode, " and company ",
                     companyId));
            }

            _cet = _cetManager.addCET(
               ConfigurableUtil.createConfigurable(
                  CETConfiguration.class, properties),
               companyId, externalReferenceCode);

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     "Deleting client extension entry relations ",
                     "for client extension ", externalReferenceCode,
                     " and company ", companyId));
            }
//          commented out code
//          _clientExtensionEntryRelLocalService.
//             deleteClientExtensionEntryRels(
//                companyId, _cet.getExternalReferenceCode());

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     "Adding client extension entry relations for ",
                     "client extension ", externalReferenceCode,
                     " and company ", companyId));
            }

            if (Objects.equals(
                  _cet.getType(),
                  ClientExtensionEntryConstants.TYPE_THEME_CSS)) {

               _addControlPanelThemeCSSClientExtensionEntryRel(
                  companyId);
            }
         }
         catch (Exception exception) {
            _log.error(
               StringBundler.concat(
                  "Unable to modify client extension ",
                  externalReferenceCode, " for company ", companyId),
               exception);

            throw exception;
         }
      });
}

Important: You need to make the same change in the @Deactivate method as well, since both methods contain the problematic code that removes relations.

Step 2: Disable the Original Factory

To disable the original implementation, create a configuration file at:

osgi/configs/com.liferay.portal.component.blacklist.internal.configuration.ComponentBlacklistConfiguration.config

With the following content:

blacklistComponentNames=[ \
  "com.liferay.client.extension.type.internal.configuration.CETConfigurationFactory" ,\
  ]

This will prevent the original component from being activated, allowing your custom implementation to take over.

Please note if you have such file already: simply add a new line there, no need to create anything new.

But What About Cleanup?

You might be wondering: "If we're preventing the deletion of these relations, what happens when we actually want to remove a Client Extension entirely?"

The good news is that this shouldn't cause any issues. When a CET (Client Extension Type) is removed, references to it become orphaned. Even if the ClientExtensionEntryRel entries remain in the database, they won't affect anything since the system checks for a valid CET before trying to load or use a Client Extension.

In other words, with the CET removed, the page won't try to load the Client Extension anyway, making the cleanup of relations unnecessary for normal operation.

Conclusion

This bug in Liferay GA132 can be quite frustrating for developers working with Client Extensions, but with the custom implementation described above, you can prevent the automatic removal of Client Extensions from pages.

The solution is straightforward to implement and shouldn't cause any issues with the normal operation of your Liferay instance. By replacing the problematic factory class, you ensure that your Client Extensions remain where you put them, even after modifications or redeployments.


Note: This solution has been tested in specific project environment. As always, thoroughly test any modifications in a staging environment before applying them to production.

Copyright: Rafał Pydyniak