Commit 4c9d511dd9f for woocommerce

commit 4c9d511dd9f3e994a3769dd2e8b49699dc2ccf5a
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date:   Tue Nov 10 19:47:13 2026 +0500

    Return 404 instead of a fatal when a variation is deleted mid-update (#70453)

    * Return 404 instead of a fatal when a variation is deleted mid-update

    * Fix variation identifier PHPStan errors without baseline entries

    * Restore variation identifier sanitization and baseline entries

    ---------

    Co-authored-by: Tom Cafferkey <tjcafferkey@gmail.com>

diff --git a/plugins/woocommerce/changelog/fix-68062-variation-deleted-mid-request b/plugins/woocommerce/changelog/fix-68062-variation-deleted-mid-request
new file mode 100644
index 00000000000..3e535ecbc3f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-68062-variation-deleted-mid-request
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Return a 404 error instead of a fatal error when a product variation is deleted while the REST API is updating it.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
index f7d36f6d057..0eca31c5781 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
@@ -393,6 +393,10 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
 			$variation = new WC_Product_Variation();
 		}

+		if ( ! $variation instanceof WC_Product_Variation ) {
+			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
+		}
+
 		// Update parent ID just once.
 		if ( 0 === $variation->get_parent_id() ) {
 			$variation->set_parent_id( absint( $request['product_id'] ) );
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
index dc91783f4f2..d82c43ff57c 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
@@ -216,6 +216,10 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
 			$variation = new WC_Product_Variation();
 		}

+		if ( ! $variation instanceof WC_Product_Variation ) {
+			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
+		}
+
 		$variation->set_parent_id( absint( $request['product_id'] ) );

 		foreach ( $customs as $property => $value ) {
@@ -238,7 +242,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
 		}

 		// MPN.
-		if ( $variation instanceof WC_Product && isset( $request['mpn'] ) ) {
+		if ( isset( $request['mpn'] ) ) {
 			$variation->set_mpn( sanitize_text_field( $request['mpn'] ) );
 		}

diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 8538d4aba71..07bc0d7bc91 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -26022,66 +26022,6 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php

-		-
-			message: '#^Cannot call method get_downloadable\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method get_parent_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_download_expiry\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_download_limit\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_downloadable\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_image_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_parent_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_sku\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_status\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Cannot call method set_virtual\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
 		-
 			message: '#^Method WC_REST_Product_Variations_V2_Controller\:\:batch_items\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
 			identifier: missingType.generics
@@ -26197,25 +26137,13 @@ parameters:
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:save_downloadable_files\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:save_product_shipping_data\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
-
-		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:set_product_images\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
+			message: '#^Parameter \#1 \$quantity of method WC_Product\:\:set_stock_quantity\(\) expects float\|null, string given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php

 		-
-			message: '#^Parameter \#1 \$quantity of method WC_Product\:\:set_stock_quantity\(\) expects float\|null, string given\.$#'
+			message: '#^Parameter \#1 \$sku of method WC_Product\:\:set_sku\(\) expects string, array\|string given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php
@@ -28935,72 +28863,12 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

-		-
-			message: '#^Cannot call method get_downloadable\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
 		-
 			message: '#^Cannot call method get_id\(\) on WC_Product\|false\|null\.$#'
 			identifier: method.nonObject
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

-		-
-			message: '#^Cannot call method set_download_expiry\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_download_limit\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_downloadable\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_global_unique_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_image_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_parent_id\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_sku\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_status\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
-		-
-			message: '#^Cannot call method set_virtual\(\) on WC_Product\|false\|null\.$#'
-			identifier: method.nonObject
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
 		-
 			message: '#^Method WC_REST_Product_Variations_Controller\:\:generate\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
 			identifier: missingType.generics
@@ -29098,43 +28966,43 @@ parameters:
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Product_Variations_Controller\:\:delete_unmatched_product_variations\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
+			message: '#^Parameter \#1 \$global_unique_id of method WC_Product\:\:set_global_unique_id\(\) expects string, array\|string given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Product_Variations_Controller\:\:get_downloads\(\) expects WC_Product_Variation, WC_Data given\.$#'
+			message: '#^Parameter \#1 \$product of method WC_REST_Product_Variations_Controller\:\:delete_unmatched_product_variations\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:get_attributes\(\) expects WC_Product, WC_Data given\.$#'
+			message: '#^Parameter \#1 \$product of method WC_REST_Product_Variations_Controller\:\:get_downloads\(\) expects WC_Product_Variation, WC_Data given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:save_downloadable_files\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
+			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:get_attributes\(\) expects WC_Product, WC_Data given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$product of method WC_REST_Products_V2_Controller\:\:save_product_shipping_data\(\) expects WC_Product, WC_Product\|false\|null given\.$#'
+			message: '#^Parameter \#1 \$quantity of method WC_Product\:\:set_stock_quantity\(\) expects float\|null, string given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$quantity of method WC_Product\:\:set_stock_quantity\(\) expects float\|null, string given\.$#'
+			message: '#^Parameter \#1 \$request of method WC_REST_Product_Variations_Controller\:\:set_cogs_info_in_product_object\(\) expects Automattic\\WooCommerce\\Internal\\CostOfGoodsSold\\WP_Rest_Request, WP_REST_Request given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

 		-
-			message: '#^Parameter \#1 \$request of method WC_REST_Product_Variations_Controller\:\:set_cogs_info_in_product_object\(\) expects Automattic\\WooCommerce\\Internal\\CostOfGoodsSold\\WP_Rest_Request, WP_REST_Request given\.$#'
+			message: '#^Parameter \#1 \$sku of method WC_Product\:\:set_sku\(\) expects string, array\|string given\.$#'
 			identifier: argument.type
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
@@ -29163,12 +29031,6 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php

-		-
-			message: '#^Parameter \#1 \$variation of method WC_REST_Product_Variations_Controller\:\:set_variation_image\(\) expects WC_Product_Variation, WC_Product\|false\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php
-
 		-
 			message: '#^Parameter \#2 \$data of method WC_REST_Products_V2_Controller\:\:save_product_shipping_data\(\) expects array, WP_REST_Request given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
index 879e45ce306..e1461af9627 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
@@ -1177,6 +1177,40 @@ class WC_REST_Product_Variations_Controller_Tests extends WC_Unit_Test_Case {
 		$this->assertSame( 1, $parent_product_children_deletes, 'Parent variation transients should be deleted once per batch.' );
 	}

+	/**
+	 * @testdox Preparing an update for a variation deleted mid-request returns an invalid ID error instead of a fatal error.
+	 *
+	 * @testWith ["WC_REST_Product_Variations_Controller"]
+	 *           ["WC_REST_Product_Variations_V2_Controller"]
+	 *
+	 * @param string $controller_class The variations controller class under test.
+	 */
+	public function test_prepare_object_returns_error_when_variation_deleted( string $controller_class ): void {
+		$parent       = WC_Helper_Product::create_variation_product();
+		$variation_id = (int) $parent->get_children()[0];
+
+		$request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $parent->get_id() . '/variations/' . $variation_id );
+		$request->set_url_params(
+			array(
+				'product_id' => $parent->get_id(),
+				'id'         => $variation_id,
+			)
+		);
+		$request->set_body_params( array( 'regular_price' => '9.99' ) );
+
+		// Simulate a concurrent deletion between the update_item() guard and preparation.
+		wp_delete_post( $variation_id, true );
+
+		$controller     = new $controller_class();
+		$prepare_method = new ReflectionMethod( $controller, 'prepare_object_for_database' );
+		$prepare_method->setAccessible( true );
+		$result = $prepare_method->invoke( $controller, $request, false );
+
+		$this->assertWPError( $result );
+		$this->assertSame( 'woocommerce_rest_product_variation_invalid_id', $result->get_error_code() );
+		$this->assertSame( 404, $result->get_error_data()['status'] );
+	}
+
 	/**
 	 * @testdox Creating a variation normalizes customs values.
 	 */