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.
*/