Commit 38b0d03f852 for woocommerce
commit 38b0d03f852398ee5fb3625f5195f7897e34821e
Author: Peerapat Pongnipakorn <39871299+Peerapatfc@users.noreply.github.com>
Date: Mon Nov 9 14:10:42 2026 +0700
Stop reusing cached Helper update data after its package links may have expired (#70343)
* Stop reusing cached Helper update data after its package links may have expired
The update check response is cached and treated as fresh for 12 hours, but
the presigned package URLs in it expire 12 hours after the check. A
twice-daily update run could be served the cache just as the links expired
and fail to download WooCommerce.com extensions with a 403.
Treat the cache as fresh for 11 hours instead.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Qualify the changelog entry for the Helper update cache change
The change shortens the normal cache freshness window. A failed check or
backoff can still return cached products whose links have expired.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Test that a failed update check drops the forced auto-update from cache older than the link lifetime
The freshness window is also used when serving cached products after a failed
check, where the server's forced auto-update decision is only kept for fresh
data. Cover the 10 and 11.5 hour boundary.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Thilina Pituwala <thilina.hasantha@gmail.com>
diff --git a/plugins/woocommerce/changelog/70315-helper-update-cache-link-expiry b/plugins/woocommerce/changelog/70315-helper-update-cache-link-expiry
new file mode 100644
index 00000000000..550eb318286
--- /dev/null
+++ b/plugins/woocommerce/changelog/70315-helper-update-cache-link-expiry
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Shorten how long the Helper reuses cached update data, so extension auto-updates are less likely to fail on expired package links.
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
index 049091f73b0..3071438a351 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
@@ -1097,13 +1097,18 @@ class WC_Helper_Updater {
/**
* Whether cached update data is recent enough to use without a new check. The cache is kept
- * for a week so a failed check has something to fall back on, but it's only fresh for 12 hours.
+ * for a week so a failed check has something to fall back on, but it's only fresh for 11 hours.
+ *
+ * The package URLs in the response are presigned links that WooCommerce.com sets to expire
+ * 12 hours after the check, so the cache has to stop being fresh before they do. Otherwise
+ * a twice-daily update run, which starts about 12 hours after the last check, could be handed
+ * links that have just expired.
*
* @param array $data The cached update data.
* @return bool
*/
private static function is_fresh_update_data( array $data ): bool {
- return isset( $data['updated'] ) && is_numeric( $data['updated'] ) && (int) $data['updated'] > time() - 12 * HOUR_IN_SECONDS;
+ return isset( $data['updated'] ) && is_numeric( $data['updated'] ) && (int) $data['updated'] > time() - 11 * HOUR_IN_SECONDS;
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
index 929378239b4..b8ed8299f2d 100644
--- a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-updater-test.php
@@ -270,6 +270,58 @@ class WC_Helper_Updater_Test extends WC_Unit_Test_Case {
$this->assertEquals( $cached_data['products'], $result, 'Result should match cached version' );
}
+ /**
+ * @testdox Cached update data should be reused only while its presigned package links are still valid.
+ *
+ * @testWith [10, 0]
+ * [11.5, 1]
+ *
+ * @param float $age_in_hours How long ago the cached update check was made.
+ * @param int $expected_requests How many update-check requests are expected.
+ */
+ public function test_update_check_reuses_cache_only_within_package_link_lifetime( float $age_in_hours, int $expected_requests ): void {
+ $payload = array(
+ 123 => array(
+ 'product_id' => 123,
+ 'file_id' => 'abc123',
+ ),
+ );
+
+ ksort( $payload );
+
+ set_transient(
+ '_woocommerce_helper_updates',
+ array(
+ 'hash' => md5( wp_json_encode( $payload ) ),
+ 'updated' => time() - (int) ( $age_in_hours * HOUR_IN_SECONDS ),
+ 'products' => array( 123 => array( 'version' => '1.2.3' ) ),
+ 'errors' => array(),
+ ),
+ WEEK_IN_SECONDS
+ );
+
+ $requests = 0;
+ $http_mock = static function () use ( &$requests ) {
+ ++$requests;
+ return array(
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => wp_json_encode( array( 123 => array( 'version' => '1.3.0' ) ) ),
+ );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ try {
+ $this->call_update_check( $payload );
+ } finally {
+ remove_filter( 'pre_http_request', $http_mock );
+ }
+
+ $this->assertSame( $expected_requests, $requests );
+ }
+
/**
* @testdox A rate-limited update check should keep the cached products instead of caching the empty result.
*/
@@ -403,6 +455,60 @@ class WC_Helper_Updater_Test extends WC_Unit_Test_Case {
$this->assertLessThanOrEqual( time() + 15 * MINUTE_IN_SECONDS, $backoff_until, 'The backoff after a failed check should be short' );
}
+ /**
+ * @testdox A failed update check should keep the forced auto-update only while the cached data is fresh, as the flag and the package links come from the same response.
+ *
+ * @testWith [10, true]
+ * [11.5, false]
+ *
+ * @param float $age_in_hours How long ago the cached update check was made.
+ * @param bool $expect_autoupdate Whether the forced auto-update is expected to be kept.
+ */
+ public function test_update_check_failure_keeps_forced_autoupdate_only_while_cache_is_fresh( float $age_in_hours, bool $expect_autoupdate ): void {
+ $payload = array(
+ 123 => array(
+ 'product_id' => 123,
+ 'file_id' => 'abc123',
+ ),
+ );
+
+ ksort( $payload );
+
+ set_transient(
+ '_woocommerce_helper_updates',
+ array(
+ 'hash' => md5( wp_json_encode( $payload ) ),
+ 'updated' => time() - (int) ( $age_in_hours * HOUR_IN_SECONDS ),
+ 'products' => array(
+ 123 => array(
+ 'version' => '1.2.3',
+ 'autoupdate' => true,
+ ),
+ ),
+ 'errors' => array(),
+ ),
+ WEEK_IN_SECONDS
+ );
+
+ $http_mock = static function () {
+ return new WP_Error( 'http_request_failed', 'cURL error 28: Operation timed out' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ try {
+ $result = $this->call_update_check( $payload );
+ } finally {
+ remove_filter( 'pre_http_request', $http_mock );
+ }
+
+ $expected = array( 123 => array( 'version' => '1.2.3' ) );
+ if ( $expect_autoupdate ) {
+ $expected[123]['autoupdate'] = true;
+ }
+
+ $this->assertSame( $expected, $result );
+ }
+
/**
* @testdox A failed update check with nothing cached should return no products and not cache the empty result.
*/