Commit b7d54e003b9 for woocommerce
commit b7d54e003b93771b5f832bfd7e7745267e519012
Author: AbdulRaheem Alick <99078532+abdulraheemalick@users.noreply.github.com>
Date: Wed Nov 4 00:17:03 2026 +0800
Fix the collections store ignoring the Store API Last-Modified date (#70363)
* Fix the collections store ignoring the Store API Last-Modified date
Since 10.7.0 the Store API products route sends Last-Modified as an
HTTP-date, which parseInt reads as NaN, so the collections store never
invalidated its cached collections. Read the header as a date, still
accept a Unix timestamp, and ignore a value that cannot be read.
* Read the Last-Modified header only as an HTTP-date
Nothing in WooCommerce has sent Last-Modified as a Unix timestamp since
10.7.0, so remove the integer branch and its test, and inline the parse
in getCollection(), as the review asked. A value that Date.parse()
cannot read gives NaN and is still skipped. The collections tests pass
(25 of 25), and ESLint reports nothing on the two files.
* Use a generic changelog entry and a stricter test check
The review found that an outdated product list is very hard to
reproduce in the UI, so the changelog entry now names the store fix
instead of a visible effect. The test for an unreadable header now
compares the whole action, as the review suggested. The collections
tests pass (25 of 25), the changed test still fails against trunk's
resolvers.js, and ESLint reports nothing on the test file.
diff --git a/plugins/woocommerce/changelog/fix-store-api-collections-last-modified-header b/plugins/woocommerce/changelog/fix-store-api-collections-last-modified-header
new file mode 100644
index 00000000000..044e2bb6c65
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-api-collections-last-modified-header
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix the collections data store ignoring the Store API Last-Modified header.
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/resolvers.js b/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/resolvers.js
index df7b96c4876..3a4eb7e2860 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/resolvers.js
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/resolvers.js
@@ -58,11 +58,16 @@ export function* getCollection( namespace, resourceName, query, ids ) {
yield apiFetchWithHeadersControl( { path: route + queryString } );
if ( headers && headers.get && headers.has( 'last-modified' ) ) {
+ // The header is an HTTP-date. A value that cannot be parsed gives
+ // NaN and is ignored.
+ const lastModified = Math.floor(
+ Date.parse( headers.get( 'last-modified' ) ) / 1000
+ );
// Do any invalidation before the collection is received to prevent
// this query running again.
- yield invalidateModifiedCollection(
- parseInt( headers.get( 'last-modified' ), 10 )
- );
+ if ( lastModified ) {
+ yield invalidateModifiedCollection( lastModified );
+ }
}
yield receiveCollection( namespace, resourceName, queryString, ids, {
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/test/resolvers.js b/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/test/resolvers.js
index b481e659805..585a83ef97f 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/test/resolvers.js
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/collections/test/resolvers.js
@@ -125,6 +125,78 @@ describe( 'getCollection', () => {
} );
} );
+describe( 'getCollection with a Last-Modified header', () => {
+ const testArgs = [ 'wc/store/v1', 'products', { per_page: 1 }, [] ];
+ const httpDate = 'Tue, 20 Oct 2026 02:19:18 GMT';
+ const timestamp = 1792462758;
+
+ const fetchWithHeader = ( lastModified ) => {
+ const headers = {
+ get: ( name ) =>
+ name === 'last-modified' ? lastModified : undefined,
+ has: ( name ) => name === 'last-modified',
+ };
+ const fulfillment = getCollection( ...testArgs );
+ fulfillment.next();
+ fulfillment.next( 'https://example.org/wc/store/v1/products' );
+ const { value } = fulfillment.next( { response: [], headers } );
+ return { value, headers };
+ };
+
+ const runInvalidation = ( invalidation, storedLastModified ) => {
+ invalidation.next();
+ invalidation.next( storedLastModified );
+ invalidation.next();
+ invalidation.next();
+ };
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'stores the time of an HTTP-date header as a Unix timestamp', () => {
+ const { value } = fetchWithHeader( httpDate );
+ runInvalidation( value, 0 );
+ expect( controls.dispatch ).toHaveBeenCalledWith(
+ STORE_KEY,
+ 'receiveLastModified',
+ timestamp
+ );
+ } );
+
+ it( 'invalidates the store when an HTTP-date header is later than the stored time', () => {
+ const { value } = fetchWithHeader( httpDate );
+ runInvalidation( value, timestamp - 60 );
+ expect( controls.dispatch ).toHaveBeenCalledWith(
+ STORE_KEY,
+ 'invalidateResolutionForStore'
+ );
+ expect( controls.dispatch ).toHaveBeenCalledWith(
+ STORE_KEY,
+ 'receiveLastModified',
+ timestamp
+ );
+ } );
+
+ it( 'does not invalidate the store when the header matches the stored time', () => {
+ const { value } = fetchWithHeader( httpDate );
+ runInvalidation( value, timestamp );
+ expect( controls.dispatch ).not.toHaveBeenCalled();
+ } );
+
+ it( 'ignores a header it cannot read', () => {
+ const { value, headers } = fetchWithHeader( 'not a date' );
+ const expected = receiveCollection(
+ 'wc/store/v1',
+ 'products',
+ '?per_page=1',
+ [],
+ { items: [], headers }
+ );
+ expect( value ).toEqual( expected );
+ } );
+} );
+
describe( 'getCollectionHeader', () => {
let fulfillment;
const rewind = ( ...testArgs ) =>