Magento 2 Bug Fixes - URL Key for specified store already exists

Magento 2 bug fixes. Swatting the proverbial fly one tutorial at a time.

This article details a code fix for the Magento 2 bug where you see the error “URL Key for specified store already exists” when you save products and/or categories. There is also a downloadable module that you can use for your store.

**Please note that this module will not be compatible with versions of Magento 2.1.8 and above.

Download the HE UrlkeyRewrite Module

The Issue with url_rewrites in Magento 2

We recently helped a client out with a Magento 2.x bug that has been hanging out in the open issues on GitHub for awhile. We have seen this bug present itself in a variety of different ways. Tt seems mostly related to multi-store Magento 2 instances with products that have been either created via the API or the native product importer. The following error is thrown when trying to save categories on the products that were imported.

URL key for specified store already exists

You can find additional information on this issue on Stack Overflow and GitHub:
https://github.com/magento/magento2/issues/6671

If you are using the API to import products with a category on it, you may get an error that reads something simply like:

Bad Request

A workaround when using the API or importing products

To first get around that, try setting these two keys in your request:

1. url_key_create_redirect => '' 
2. save_rewrites_history => false

This should allow you to create products with categories. If you try to subsequently save the product, with or without additional categories, it will still throw the same error. This problem can be traced into the

vendor/magento/module-catalog-url-rewrite

module where the error actually presents itself.

The core of the problem

The actual problem is that Magento is incorrectly assigning the ‘store_id’ of the current store to url rewrites that are part of the original ‘Root Catalog’ category. This is the category with entity_id “1”. It was a little unclear why and when it was generating those urls and inserting them into the `url_rewrite` table, so we had to step through the code to find when these functions were getting called.

As it turned out, the magento-catalog-url-rewrite module has observers that kicked off this process based on these events.

[code]
catalog_category_prepare_save
catalog_category_save_after
catalog_product_import_bunch_save_after
catalog_product_import_bunch_delete_after
catalog_product_delete_before
catalog_product_save_before
catalog_product_save_after
catalog_category_save_before
catalog_category_move_after
[/code]

MySQL throws errors related to an index requiring a unique key on store_id

Then after everything runs we get to the MySQL query which causes the database to throw an error. When the error is thrown, it stops the rest of the processes from running and returns the error specified above.

[code]
INSERT INTO `url_rewrite`
(`redirect_type`,`is_autogenerated`,`metadata`,`description`,`entity_type`,`entity_id`,`request_path`,`target_path`,`store_id`)

VALUES
(0, 1, NULL, NULL, ‘product’, ‘6107’, ‘xyz.html’, ‘catalog/product/view/id/6107’, ‘1’),
(0, 1, ‘a:1:{s:11:"category_id";s:3:"345";}’, NULL, ‘product’, ‘6107’, ‘temp-category-testing/xyz.html’, ‘catalog/product/view/id/6107/category/345’, ‘1’),
(0, 1, ‘a:1:{s:11:"category_id";s:1:"1";}’, NULL, ‘product’, ‘6107’, ‘/xyz.html’, ‘catalog/product/view/id/6107/category/1’, ‘1’),
(0, 1, ‘a:1:{s:11:"category_id";s:3:"305";}’, NULL, ‘product’, ‘6107’, ‘/xyz.html’, ‘catalog/product/view/id/6107/category/305’, ‘1’)
[/code]

This query throws this error.

Duplicate entry ‘/xyz.html-1’ for key ‘URL_REWRITE_REQUEST_PATH_STORE_ID’

In particular, this was the value that seemed incorrect is this one:

(0, 1, ‘a:1:{s:11:”category_id”;s:1:”1″;}’, NULL, ‘product’, ‘6107’, ‘/xyz.html’, ‘catalog/product/view/id/6107/category/1’, ‘1’)

Category ID 1 is reserved for the Root Catalog and it was trying to apply the ‘store_id’ of the current scope store and not the ‘store_id’ of the default config, or “0”. In most multi-store Magento applications, it shouldn’t matter if this value is set to “0” because the category_id of the previous record for the store is really the only one that we care about.

Now that we understand what the problem is, we can back up in the code to find where it generated the $urls variable which it used in the end as the $bind parameters for the call to insert multiple records into the database. There are four methods that were called individually that handled all of this in the following file.

\Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator

The code fix that will eliminate this error when saving products and categories

Here is the proposed modification to the core file. This will take into account if one of the category ids is the Root Catalog. Before the foreach loop, set a current store variable to keep track of the store id that was passed in. If during the loop we find that we are dealing with the Root Catalog category id, set the store id to zero to prevent the duplicate url key issue. Subsequent trips through the loop would set back the original store id that was passed into the method. Making this modification allowed me to save the product with categories assigned to them.

[code]
public function generate($storeId, Product $product, ObjectRegistry $productCategories)
{
$urls = [];
$currentStore = $storeId;
foreach ($productCategories->getList() as $category) {
$anchorCategoryIds = $category->getAnchorsAbove();
if ($anchorCategoryIds) {
foreach ($anchorCategoryIds as $anchorCategoryId) {
//Default: $anchorCategory = $this->categoryRepository->get($anchorCategoryId);
$storeId = ($anchorCategoryId === "1" ? "0" : $currentStore);

$anchorCategory = $this->categoryRepository->get($anchorCategoryId, $storeId);
$urls[] = $this->urlRewriteFactory->create()
->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
->setEntityId($product->getId())
->setRequestPath(
$this->urlPathGenerator->getUrlPathWithSuffix(
$product,
$storeId,
$anchorCategory
)
)
->setTargetPath(
$this->urlPathGenerator->getCanonicalUrlPath(
$product,
$anchorCategory
)
)
->setStoreId($storeId)
->setMetadata([‘category_id’ => $anchorCategory->getId()]);
}
}
}

return $urls;
}
[/code]

Here is the resulting url_rewrite table view where you can see the record that has a store id of zero. It does not conflict now with the other rewrite for the current store. The default product ‘request_paths’ seem to be unaffected because the do not get stored with the leading forward slash.

url_rewrite_view

A downloadable package:

The attached downloadable package can be pasted into the app/code directory of your store and will apply the fix mentioned above. It contains two preferences and two method overrides. Once you have installed the module, make sure to flush all of the caches and run setup:di:compile. If this works for you, please comment and let me know!

Download the HE UrlkeyRewrite Module