Common Workarounds

There is what looks like a core bug in Magento 2 where images inserted into both Category images and CMS content images are displayed as a broken link on the frontend. Inspecting the image URL will show that it's along the lines of admin_frontname/cms/wysiwyg/directive/___directive/randomstring - its temporary URL as used by the Admin to display the image. This is being stored in the database and served up on the frontend, which is obviously not accessible.

A simple workaround for this is to disable the WYSIWYG editor just above the content area. It'll need a bit of basic HTML knowledge to find your place, but avoids the headache of broken images.

  1. Click Show/Hide Editor so that the toolbars disappear
  2. Click Insert Image
  3. Select your image
  4. Click Insert Image
  5. Press Save Page

It should now appear correctly.

Attribute not saving on categories

This often occurs after a migration is completed from Magento 1, the attribute can be re-added to a category edit page using the standard Magento 2 way (not covered here) and it will be displayed and be editable but will not update on save.

What is happening is that the migrated custom attribute is assigned to the wrong attribute set. After a migration, there will be at least 2 attribute sets for a category, only one will be used by default and there will be no option to choose which set to use (in Open Source M2 any way).

To get a list of all the attributes and what they are set to use, use the following query.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- Get attributes with assignments for type "catalog_category"
SELECT s.attribute_set_name, g.attribute_group_name, a.attribute_code, a.frontend_label, ea.sort_order
FROM eav_attribute_set s
       LEFT JOIN eav_attribute_group g ON s.attribute_set_id = g.attribute_set_id
       LEFT JOIN eav_entity_attribute ea ON g.attribute_group_id = ea.attribute_group_id
       LEFT JOIN eav_attribute a ON ea.attribute_id = a.attribute_id
WHERE s.entity_type_id = (SELECT entity_type_id FROM eav_entity_type WHERE entity_type_code = 'catalog_category')
ORDER BY s.attribute_set_name,
         g.sort_order,
         ea.sort_order;

To display the default set that is being used by a category then you can use the following query.

1
2
3
4
5
-- Show the eav attribute set used by default
SELECT *
FROM eav_entity_attribute
WHERE entity_type_id = (SELECT entity_type_id FROM eav_entity_type WHERE entity_type_code = 'catalog_category')
  AND attribute_id = ?;

To fix the issue, you need to change the attribute_set_id to the correct default used by your entity (e.g. catalog_category).

Varnish and Nginx Errors on Category Pages

For some incredibly stupid reason, Magento decided to use a concatenated string of every product in a category, including both the product id integer the string catalog_product_, so for each product in the category we get catalog_product_{{PRODUCT_ID}}. Headers can easily become tens of KB in size!

X-Magento-Tags header too large #6401

Troubleshooting Varnish with Magento 2

So Why Do They Do This?

They do this so that Varnish can be surgicly flushed based upon a per product basis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 if (req.method == "PURGE") {
        if (client.ip !~ purge) {
            return (synth(405, "Method not allowed"));
        }
        if (!req.http.X-Magento-Tags-Pattern) {
            return (synth(400, "X-Magento-Tags-Pattern header required"));
        }
        ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
        return (synth(200, "Purged"));
    }

They send a purge request which uses Varnish's ban functionality to invalidate any pages, or objects in Varnish speak, that match against that header.

article

Check Raw FPM Output

You might not even be able to see the raw output due to Nginx throwing errors. In this case you can use the technique described here:

Debugging FPM

This will allow you to see the header that is causing problems

To count the size of the header, you can use the above technique and pipe the output like this:

1
fmpscript | grep 'X-Magento-Tags' | wc -c

Which will then give you the number of characters

Mitigate By Increasing Limits

First level of mitigation is to increase limits in Nginx and Varnish

Follow these instructions to increase Varnish limits: Magento docs on Varnish

Read through this to understand Nginx header size limits: Nginx FastCGI response buffer sizes

Fix by Modifying Magento Functionality

The next level is to fix this by modifying this behaviour so that the header is not stupidly big.

There are a few strategies here:

Plugin to remove simple product tags

Product attributes not updating all values

This may be due to the large number of attribute options in the attribute, to check - see the error log and search for some thing like the following:

1
FastCGI sent in stderr: "PHP message: PHP Warning:  Unknown: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0" while reading response header from upstream, client: 127.0.0.1, server: www.magento2.client.developmagento.co.uk, request: "POST /admin/catalog/product_attribute/validate/attribute_id/700/?isAjax=true

To fix the issue, update the php.ini file and increase max_input_vars

1
2
; How many GET/POST/COOKIE input variables may be accepted
max_input_vars = 2000

Category Caches not clearing

As mentioned above, Magento makes extensive use of tags when caching things. This can be helpful if you need to cache category or product specific information and have it refreshed when something changes.

Unfortunately the category cache flushing only appears to clear the Full Page Cache, and not any others when a category is saved.

This can be fixed using a plugin before the category save event with the following code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace EdmondsCommerce\CacheClearer\Plugin\Magento\Catalog\Model;

use Magento\Catalog\Model\Category as OriginalClass;
use Magento\Framework\Model\Context;

class Category
{
    private $cacheManagement;

    /**
     * Category constructor.
     *
     * @param Context $context
     */
    public function __construct(Context $context)
    {
        $this->cacheManagement = $context->getCacheManager();
    }

    public function beforeSave(OriginalClass $subject)
    {
        if ($subject->hasDataChanges() !== false) {
            $this->cacheManagement->clean([OriginalClass::CACHE_TAG . '_' . $subject->getId()]);
        }
    }
}