Commit 921702407b5 for woocommerce
commit 921702407b509703f9e885cef22ac4a17afc5980
Author: Soroush Ahmadi <mrsoroushahmadi@gmail.com>
Date: Wed Sep 23 13:50:13 2026 +0330
Fix cart blocks crash on corrupt localStorage cart cache (#68372)
* Fix cart blocks crash on corrupt localStorage cart cache
* Fix Cart return contract on persistence layer get
* Wrap the whole persistence get in try/catch
* fix TS errors from Cart|null return contract
* Fix linting in test file
---------
Co-authored-by: soroush5 <soroush5@users.noreply.github.com>
Co-authored-by: Tom Cafferkey <tjcafferkey@gmail.com>
diff --git a/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache b/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache
new file mode 100644
index 00000000000..6af17980066
--- /dev/null
+++ b/plugins/woocommerce/changelog/68370-fix-cart-persistence-corrupt-cache
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent the cart, checkout and mini-cart blocks from crashing when the cached cart data in localStorage is corrupt. The persistence layer now drops the bad cache and re-fetches instead of throwing.
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
index 1ab93bf2dfb..331a6e04d41 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/index.ts
@@ -62,7 +62,7 @@ window.addEventListener( 'load', () => {
const cachedCart = persistenceLayer.get();
// On login, if a customer had a cart session, the cached cart is equal to the default cart data, with no items.
// We need to check if the cached cart has items, otherwise we will wrongly skip the API request.
- const hasItemsInCachedCart = cachedCart?.itemsCount > 0;
+ const hasItemsInCachedCart = ( cachedCart?.itemsCount ?? 0 ) > 0;
if (
( ! hasCartSession() || hasItemsInCachedCart ) &&
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
index 4f12fb435e1..c0a1c88e6dc 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/persistence-layer.ts
@@ -33,24 +33,34 @@ export const isAddingToCart = () => {
};
export const persistenceLayer = {
- get: () => {
- if ( ! hasCartSession() || ! hasValidHash() ) {
- return null;
- }
+ get: (): Cart | null => {
+ try {
+ if ( ! hasCartSession() || ! hasValidHash() ) {
+ return null;
+ }
- const cached = window.localStorage?.getItem( 'storeApiCartData' );
+ const cached = window.localStorage?.getItem( 'storeApiCartData' );
- if ( ! cached ) {
- return null;
- }
+ if ( ! cached ) {
+ return null;
+ }
+
+ const parsed: unknown = JSON.parse( cached );
- const parsed = JSON.parse( cached );
+ if ( ! parsed || typeof parsed !== 'object' ) {
+ return null;
+ }
- if ( ! parsed || typeof parsed !== 'object' ) {
+ return parsed as Cart;
+ } catch {
+ // Best-effort read: `get` runs at store creation, so any throw
+ // here crashes cart, checkout, and mini-cart alike. Covered:
+ // corrupt JSON, and `window.localStorage` access itself throwing
+ // (private-mode Safari, disabled storage, jsdom teardown in
+ // Jest) — the same cases `set` below guards against. Fall back
+ // to null so the store re-fetches.
return null;
}
-
- return parsed;
},
set: ( cartData: Cart ) => {
// Wrap in try/catch for two reasons:
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
index 24e5b6ac78a..f1e13858fd4 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/index.ts
@@ -2,6 +2,7 @@
* External dependencies
*/
import { dispatch as wpDispatch } from '@wordpress/data';
+import type { Cart } from '@woocommerce/types';
/**
* Internal dependencies
@@ -27,13 +28,16 @@ const mockWpDispatch = jest.mocked( wpDispatch );
describe( 'Window load event handler', () => {
let mockFinishResolution: jest.Mock;
let originalAddEventListener: typeof window.addEventListener;
- let loadHandler: EventListener;
+ let loadHandler: ( event: Event ) => void;
beforeAll( () => {
// Capture the addEventListener calls to extract the load handler
originalAddEventListener = window.addEventListener;
window.addEventListener = jest.fn(
- ( event: string, handler: EventListenerOrEventListenerObject ) => {
+ (
+ event: string,
+ handler: Parameters< typeof window.addEventListener >[ 1 ]
+ ) => {
if ( event === 'load' && typeof handler === 'function' ) {
loadHandler = handler;
}
@@ -42,7 +46,7 @@ describe( 'Window load event handler', () => {
);
// Now import the module to register the event listener
- require( '../index' );
+ jest.requireActual( '../index' );
} );
beforeEach( () => {
@@ -69,7 +73,9 @@ describe( 'Window load event handler', () => {
it( 'should skip API request when cached cart has items and not adding to cart with /?add-to-cart=', () => {
mockHasCartSession.mockReturnValue( true );
mockIsAddingToCart.mockReturnValue( false );
- mockPersistenceLayerGet.mockReturnValue( { itemsCount: 2 } );
+ mockPersistenceLayerGet.mockReturnValue( {
+ itemsCount: 2,
+ } as unknown as Cart );
loadHandler( new Event( 'load' ) );
@@ -79,7 +85,9 @@ describe( 'Window load event handler', () => {
it( 'should make API request when has cart session but cached cart is empty', () => {
mockHasCartSession.mockReturnValue( true );
mockIsAddingToCart.mockReturnValue( false );
- mockPersistenceLayerGet.mockReturnValue( { itemsCount: 0 } );
+ mockPersistenceLayerGet.mockReturnValue( {
+ itemsCount: 0,
+ } as unknown as Cart );
loadHandler( new Event( 'load' ) );
@@ -109,7 +117,9 @@ describe( 'Window load event handler', () => {
it( 'should make API request when has cart session, cached cart has items, but adding to cart with /?add-to-cart=', () => {
mockHasCartSession.mockReturnValue( true );
mockIsAddingToCart.mockReturnValue( true );
- mockPersistenceLayerGet.mockReturnValue( { itemsCount: 2 } );
+ mockPersistenceLayerGet.mockReturnValue( {
+ itemsCount: 2,
+ } as unknown as Cart );
loadHandler( new Event( 'load' ) );
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js
new file mode 100644
index 00000000000..d6cb1ba5a30
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/persistence-layer.js
@@ -0,0 +1,46 @@
+/**
+ * Internal dependencies
+ */
+import { persistenceLayer } from '../persistence-layer';
+
+describe( 'persistenceLayer', () => {
+ beforeEach( () => {
+ window.localStorage.clear();
+ document.cookie = 'woocommerce_items_in_cart=1';
+ document.cookie = 'woocommerce_cart_hash=abc';
+ window.localStorage.setItem( 'storeApiCartHash', 'abc' );
+ } );
+
+ it( 'returns null instead of throwing when the cached cart is corrupt', () => {
+ window.localStorage.setItem( 'storeApiCartData', '{corrupt-json' );
+ expect( () => persistenceLayer.get() ).not.toThrow();
+ expect( persistenceLayer.get() ).toBeNull();
+ } );
+
+ it( 'returns the parsed cart when the cached cart is valid', () => {
+ window.localStorage.setItem(
+ 'storeApiCartData',
+ JSON.stringify( { itemsCount: 2 } )
+ );
+ expect( persistenceLayer.get() ).toEqual( { itemsCount: 2 } );
+ } );
+
+ it( 'returns null instead of throwing when localStorage access throws', () => {
+ const storage = window.localStorage;
+ Object.defineProperty( window, 'localStorage', {
+ configurable: true,
+ get() {
+ throw new Error( 'denied' );
+ },
+ } );
+ try {
+ expect( () => persistenceLayer.get() ).not.toThrow();
+ expect( persistenceLayer.get() ).toBeNull();
+ } finally {
+ Object.defineProperty( window, 'localStorage', {
+ configurable: true,
+ value: storage,
+ } );
+ }
+ } );
+} );