Commit b39ce3a2aa1 for woocommerce
commit b39ce3a2aa1d58832e6111ae9bccc098007b90c6
Author: Mustafa Bharmal <92981596+Mustafabharmal@users.noreply.github.com>
Date: Thu Oct 22 17:53:31 2026 +0530
Products: Avoid duplicating the featured image in REST API and Store API images (#70134)
diff --git a/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images b/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images
new file mode 100644
index 00000000000..2dd5c820da4
--- /dev/null
+++ b/plugins/woocommerce/changelog/38462-fix-duplicate-featured-image-in-product-images
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop the product REST API and Store API from listing the featured image twice when it is also in the product gallery.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
index a7b3e0843bf..d3cb94cccef 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php
@@ -554,8 +554,8 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
$attachment_ids[] = $product->get_image_id();
}
- // Add gallery images.
- $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+ // Add gallery images, skipping the featured image if it is also in the gallery.
+ $attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );
if ( ! empty( $attachment_ids ) ) {
// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
index 09b221e5710..077ed424283 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php
@@ -184,8 +184,8 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$attachment_ids[] = $product->get_image_id();
}
- // Add gallery images.
- $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+ // Add gallery images, skipping the featured image if it is also in the gallery.
+ $attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );
if ( ! empty( $attachment_ids ) ) {
// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
index e449959c135..2c7b566b489 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Products/Controller.php
@@ -298,8 +298,8 @@ class Controller extends WC_REST_Products_V2_Controller {
$attachment_ids[] = $product->get_image_id();
}
- // Add gallery images.
- $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
+ // Add gallery images, skipping the featured image if it is also in the gallery.
+ $attachment_ids = array_merge( $attachment_ids, array_diff( $product->get_gallery_image_ids(), $attachment_ids ) );
if ( ! empty( $attachment_ids ) ) {
// Prime caches to reduce future queries.
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index ca2ae649e41..486e2a7628f 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -691,7 +691,7 @@ class ProductSchema extends AbstractSchema {
* @return array
*/
protected function get_images( \WC_Product $product ) {
- $attachment_ids = array_filter( array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() ) );
+ $attachment_ids = array_unique( array_filter( array_merge( [ $product->get_image_id() ], $product->get_gallery_image_ids() ) ) );
if ( ! empty( $attachment_ids ) ) {
// Prime caches to reduce future queries.
_prime_post_caches( $attachment_ids );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
index ccd73a6a10c..8eacee85f2d 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
@@ -2895,4 +2895,27 @@ class WC_REST_Products_Controller_Tests extends WC_Unit_Test_Case {
$this->assertSame( 'rest_invalid_param', $response->get_data()['code'], 'Schema validation should reject the non-string value.' );
$this->assertFalse( metadata_exists( 'post', $product->get_id(), '_customs_commodity_code' ), 'A rejected commodity code should not be stored.' );
}
+
+ /**
+ * @testdox The featured image is returned only once when it is also part of the product gallery.
+ */
+ public function test_get_product_does_not_duplicate_featured_image_in_gallery(): void {
+ $image_file = WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png';
+ $featured_id = self::factory()->attachment->create_upload_object( $image_file );
+ $gallery_id = self::factory()->attachment->create_upload_object( $image_file );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_image_id( $featured_id );
+ $product->set_gallery_image_ids( array( $featured_id, $gallery_id ) );
+ $product->save();
+
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) );
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertSame(
+ array( $featured_id, $gallery_id ),
+ wp_list_pluck( $response->get_data()['images'], 'id' ),
+ 'The featured image should be listed first and only once.'
+ );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
index 09950d28347..1dc28ca8c6c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
@@ -396,6 +396,31 @@ class Products extends ControllerTestCase {
$this->assertCount( 0, $data['images'] );
}
+ /**
+ * @testdox The featured image is returned only once when it is also part of the product gallery.
+ */
+ public function test_featured_image_in_gallery_is_not_duplicated(): void {
+ $fixtures = new FixtureData();
+ $featured_id = $fixtures->create_image_attachment();
+ $gallery_id = $fixtures->create_image_attachment();
+ $product = $fixtures->get_simple_product(
+ array(
+ 'name' => 'Test Product 1',
+ 'regular_price' => 10,
+ 'image_id' => $featured_id,
+ 'gallery_image_ids' => array( $featured_id, $gallery_id ),
+ )
+ );
+
+ $response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/products/' . $product->get_id() ) );
+
+ $this->assertSame(
+ array( $featured_id, $gallery_id ),
+ wp_list_pluck( $response->get_data()['images'], 'id' ),
+ 'The featured image should be listed first and only once.'
+ );
+ }
+
/**
* Test product category image return types.
*/