Commit c198af5c818 for woocommerce
commit c198af5c818093b7954b4a6b0b4afe457b048e9e
Author: Chris Huber <chubes@extrachill.com>
Date: Fri Jun 19 12:00:08 2026 -0400
Fix shipping cache invalidation from package metadata (#65541)
* Add changefile(s) from automation for the following project(s): woocommerce
* Broaden shipping package cache hash exclusions
* Add changefile(s) from automation for the following project(s): woocommerce
* Address shipping package hash review feedback
* Fix shipping test array alignment
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>
diff --git a/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy b/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy
new file mode 100644
index 00000000000..6cbd7111076
--- /dev/null
+++ b/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent non-rate package metadata from invalidating cached shipping rates.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index 0859d0cde6d..1c9e335f90e 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -320,20 +320,12 @@ class WC_Shipping {
// local pickup rates here however since those are not shipped.
$is_shippable = $this->is_package_shippable( $package );
- // Check if we need to recalculate shipping for this package.
- $package_to_hash = $package;
-
- // Remove data objects so hashes are consistent.
- foreach ( $package_to_hash['contents'] as $item_id => $item ) {
- unset( $package_to_hash['contents'][ $item_id ]['data'] );
- }
-
// Get rates stored in the WC session data for this package.
$wc_session_key = 'shipping_for_package_' . $package_key;
$stored_rates = WC()->session->get( $wc_session_key );
// Calculate the hash for this package so we can tell if it's changed since last calculation.
- $package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
+ $package_hash = $this->get_package_hash( $package );
if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) {
foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) {
@@ -418,6 +410,46 @@ class WC_Shipping {
return $package;
}
+ /**
+ * Generate the shipping-rate cache hash for a package.
+ *
+ * @param array $package_to_hash Package of cart items.
+ * @return string
+ */
+ private function get_package_hash( array $package_to_hash ): string {
+ /**
+ * Filters package fields that should not affect the shipping-rate cache hash.
+ *
+ * @since 11.0.0
+ *
+ * @param array $ignored_fields Package field names ignored while generating the cache hash.
+ * @param array $package Package of cart items.
+ */
+ $ignored_fields = apply_filters(
+ 'woocommerce_shipping_package_hash_ignored_fields',
+ array(
+ 'subtotal',
+ 'total',
+ 'package_id',
+ 'package_name',
+ 'rates',
+ 'package_index',
+ ),
+ $package_to_hash
+ );
+
+ foreach ( array_unique( array_filter( (array) $ignored_fields, 'is_string' ) ) as $field ) {
+ unset( $package_to_hash[ $field ] );
+ }
+
+ // Remove data objects so hashes are consistent.
+ foreach ( (array) ( $package_to_hash['contents'] ?? array() ) as $item_id => $item ) {
+ unset( $package_to_hash['contents'][ $item_id ]['data'] );
+ }
+
+ return 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
+ }
+
/**
* Get packages.
*
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
index fbe43a11620..535bf37d22e 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
@@ -128,6 +128,117 @@ class WC_Shipping_Test extends WC_Unit_Test_Case {
remove_action( 'woocommerce_shipping_methods', $shipping_methods_hook );
}
+ /**
+ * @testdox ignored package fields do not invalidate cached shipping rates
+ *
+ * @dataProvider provide_ignored_package_hash_fields
+ *
+ * @param string $field Package field to mutate.
+ * @param mixed $value Mutated field value.
+ */
+ public function test_calculate_shipping_for_package_ignores_non_rate_fields_in_package_hash( string $field, $value ) {
+ update_option( 'woocommerce_shipping_debug_mode', 'no' );
+ WC()->session->__unset( 'shipping_for_package_0' );
+
+ $filter_calls = 0;
+ $filter = $this->get_package_rates_counter( $filter_calls );
+ $package = $this->get_package_hash_test_package();
+
+ add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $package[ $field ] = $value;
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $this->assertSame( 1, $filter_calls );
+
+ remove_filter( 'woocommerce_package_rates', $filter, 10 );
+ }
+
+ /**
+ * @testdox material package fields invalidate cached shipping rates
+ *
+ * @dataProvider provide_material_package_hash_fields
+ *
+ * @param callable $mutate_package Package mutation callback.
+ */
+ public function test_calculate_shipping_for_package_invalidates_cache_for_material_package_changes( callable $mutate_package ) {
+ update_option( 'woocommerce_shipping_debug_mode', 'no' );
+ WC()->session->__unset( 'shipping_for_package_0' );
+
+ $filter_calls = 0;
+ $filter = $this->get_package_rates_counter( $filter_calls );
+ $package = $this->get_package_hash_test_package();
+
+ add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $mutate_package( $package );
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $this->assertSame( 2, $filter_calls );
+
+ remove_filter( 'woocommerce_package_rates', $filter, 10 );
+ }
+
+ /**
+ * @testdox unknown package fields invalidate cached shipping rates by default
+ */
+ public function test_calculate_shipping_for_package_invalidates_cache_for_unknown_package_fields_by_default() {
+ update_option( 'woocommerce_shipping_debug_mode', 'no' );
+ WC()->session->__unset( 'shipping_for_package_0' );
+
+ $filter_calls = 0;
+ $filter = $this->get_package_rates_counter( $filter_calls );
+ $package = $this->get_package_hash_test_package();
+
+ add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $package['custom_extension_key'] = 'changed';
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $this->assertSame( 2, $filter_calls );
+
+ remove_filter( 'woocommerce_package_rates', $filter, 10 );
+ }
+
+ /**
+ * @testdox extensions can ignore package fields for the shipping-rate cache hash
+ */
+ public function test_calculate_shipping_for_package_allows_extensions_to_ignore_package_hash_fields() {
+ update_option( 'woocommerce_shipping_debug_mode', 'no' );
+ WC()->session->__unset( 'shipping_for_package_0' );
+
+ $filter_calls = 0;
+ $filter = $this->get_package_rates_counter( $filter_calls );
+ $ignored_fields_filter = function ( array $ignored_fields ): array {
+ $ignored_fields[] = 'custom_extension_key';
+ return $ignored_fields;
+ };
+ $package = $this->get_package_hash_test_package();
+
+ add_filter( 'woocommerce_package_rates', $filter, 10 );
+ add_filter( 'woocommerce_shipping_package_hash_ignored_fields', $ignored_fields_filter );
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $package['custom_extension_key'] = 'changed';
+
+ $this->sut->calculate_shipping_for_package( $package );
+
+ $this->assertSame( 1, $filter_calls );
+
+ remove_filter( 'woocommerce_shipping_package_hash_ignored_fields', $ignored_fields_filter );
+ remove_filter( 'woocommerce_package_rates', $filter, 10 );
+ }
+
/**
* Data provider for test_package_rates_filter_error_handling.
*
@@ -170,6 +281,92 @@ class WC_Shipping_Test extends WC_Unit_Test_Case {
);
}
+ /**
+ * Data provider for ignored package hash fields.
+ *
+ * @return array[]
+ */
+ public function provide_ignored_package_hash_fields(): array {
+ return array(
+ 'subtotal' => array( 'subtotal', 20 ),
+ 'total' => array( 'total', 20 ),
+ 'package_id' => array( 'package_id', 'package-1-changed' ),
+ 'package_name' => array( 'package_name', 'Package 1 Changed' ),
+ 'rates' => array( 'rates', array( 'prefilled_rate' => new WC_Shipping_Rate( 'prefilled_rate', 'Prefilled Rate', '7.00' ) ) ),
+ 'package_index' => array( 'package_index', 2 ),
+ );
+ }
+
+ /**
+ * Data provider for material package hash fields.
+ *
+ * @return array[]
+ */
+ public function provide_material_package_hash_fields(): array {
+ return array(
+ 'destination postcode' => array(
+ function ( array &$package ): void {
+ $package['destination']['postcode'] = '11111';
+ },
+ ),
+ 'contents cost' => array(
+ function ( array &$package ): void {
+ $package['contents_cost'] = 20;
+ },
+ ),
+ 'cart contents' => array(
+ function ( array &$package ): void {
+ $package['contents']['test_item']['quantity'] = 2;
+ },
+ ),
+ );
+ }
+
+ /**
+ * Get a package rates filter that counts recalculations.
+ *
+ * @param int $filter_calls Filter call count.
+ * @return callable
+ */
+ private function get_package_rates_counter( int &$filter_calls ): callable {
+ return function ( $rates ) use ( &$filter_calls ) {
+ ++$filter_calls;
+ return $rates;
+ };
+ }
+
+ /**
+ * Get a package for shipping hash tests.
+ *
+ * @return array
+ */
+ private function get_package_hash_test_package(): array {
+ return array(
+ 'contents' => array(
+ 'test_item' => array(
+ 'quantity' => 1,
+ 'line_subtotal' => 10,
+ 'line_subtotal_tax' => 0,
+ 'line_total' => 10,
+ 'line_tax' => 0,
+ 'data' => new WC_Product_Simple(),
+ ),
+ ),
+ 'contents_cost' => 10,
+ 'destination' => array(
+ 'country' => 'US',
+ 'state' => 'CA',
+ 'postcode' => '00000',
+ ),
+ 'package_id' => 'package-1',
+ 'package_name' => 'Package 1',
+ 'package_index' => 1,
+ 'subtotal' => 10,
+ 'total' => 10,
+ 'rates' => array(),
+ );
+ }
+
/**
* Data provider for test_calculate_shipping_for_hide_rates_when_free.
*