Commit 6eb03d211f2 for woocommerce
commit 6eb03d211f20013e7e197a015d5c5f61baf680e7
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Thu Aug 20 17:52:49 2026 +0100
Fix Store API fatal when the cart session fails to load (#67769)
Cart initialization ran before the route try/catch block, so any Throwable
raised while restoring the cart from the session escaped the Store API error
conversion and surfaced as a fatal 500 with no Cart-Token, Cart-Hash or Nonce
headers. Checkout duplicated the same load-before-try ordering in its own
get_response() override.
Wrap the load in both AbstractCartRoute::get_response() and Checkout's
override, log the original throwable server-side, and return a client-safe
error response through a shared get_cart_session_error_response() helper.
Cart-dependent response headers are now emitted only when a cart is available,
so header generation cannot fatal a second time on the failure path.
diff --git a/plugins/woocommerce/changelog/67124-fix-store-api-cart-session-error-boundary b/plugins/woocommerce/changelog/67124-fix-store-api-cart-session-error-boundary
new file mode 100644
index 00000000000..a1b3c65014b
--- /dev/null
+++ b/plugins/woocommerce/changelog/67124-fix-store-api-cart-session-error-boundary
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Return a Store API error response instead of a fatal error when the cart cannot be loaded from the session.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
index 98b565ae0f0..06a688097a5 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
@@ -103,6 +103,37 @@ abstract class AbstractCartRoute extends AbstractRoute {
return in_array( $request->get_method(), [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true );
}
+ /**
+ * Convert a failure during cart session loading into a client-safe error response.
+ *
+ * The original error is logged rather than returned, so nothing internal reaches the client.
+ *
+ * @param \Throwable $error The error that occurred while loading the cart session.
+ * @return \WP_REST_Response The error response to return to the client.
+ */
+ protected function get_cart_session_error_response( \Throwable $error ) {
+ wc_get_logger()->error(
+ sprintf(
+ 'Store API could not load the cart session: %1$s in %2$s:%3$d',
+ $error->getMessage(),
+ $error->getFile(),
+ $error->getLine()
+ ),
+ array(
+ 'source' => 'store-api',
+ 'exception' => $error,
+ )
+ );
+
+ return $this->error_to_response(
+ $this->get_route_error_response(
+ 'woocommerce_rest_unknown_server_error',
+ __( 'The cart could not be loaded. Please try again.', 'woocommerce' ),
+ 500
+ )
+ );
+ }
+
/**
* Get the route response based on the type of request.
*
@@ -111,7 +142,11 @@ abstract class AbstractCartRoute extends AbstractRoute {
* @return \WP_REST_Response
*/
public function get_response( \WP_REST_Request $request ) {
- $this->load_cart_session( $request );
+ try {
+ $this->load_cart_session( $request );
+ } catch ( \Throwable $error ) {
+ return $this->add_response_headers( $this->get_cart_session_error_response( $error ) );
+ }
$response = null;
$nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null;
@@ -156,10 +191,13 @@ abstract class AbstractCartRoute extends AbstractRoute {
$response->header( 'Nonce', $nonce );
$response->header( 'Nonce-Timestamp', time() );
$response->header( 'User-ID', get_current_user_id() );
- $response->header( 'Cart-Token', $this->get_cart_token() );
- $response->header( 'Cart-Hash', WC()->cart->get_cart_hash() );
$response->header( 'Cache-Control', 'no-store' );
+ if ( WC()->cart instanceof \WC_Cart ) {
+ $response->header( 'Cart-Token', $this->get_cart_token() );
+ $response->header( 'Cart-Hash', WC()->cart->get_cart_hash() );
+ }
+
return $response;
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index c33f1a42451..773fa04f6e8 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -153,7 +153,11 @@ class Checkout extends AbstractCartRoute {
* @return \WP_REST_Response
*/
public function get_response( \WP_REST_Request $request ) {
- $this->load_cart_session( $request );
+ try {
+ $this->load_cart_session( $request );
+ } catch ( \Throwable $error ) {
+ return $this->add_response_headers( $this->get_cart_session_error_response( $error ) );
+ }
$response = null;
$nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null;
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
index a1058d80d23..625628034b6 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
@@ -33,6 +33,13 @@ class Cart extends ControllerTestCase {
*/
private static $coupon_id;
+ /**
+ * Cart instance removed to mimic a REST request, restored on teardown.
+ *
+ * @var \WC_Cart|null
+ */
+ private $cart_backup = null;
+
/**
* Create immutable catalog rows shared by all test methods.
*/
@@ -1550,4 +1557,64 @@ class Cart extends ControllerTestCase {
remove_action( 'internal_woocommerce_cart_item_added_from_user_request', $callback );
remove_filter( 'woocommerce_store_api_add_to_cart_data', $add_to_cart_data_callback );
}
+
+ /**
+ * @testdox Should return an error response when restoring the cart session throws.
+ */
+ public function test_cart_session_failure_returns_error_response() {
+ wc()->session->set( 'cart', wc()->cart->get_cart_for_session() );
+
+ // The route restores the cart only when this action has not run yet, so reset
+ // the counter to put the process back into the state a REST request starts in.
+ unset( $GLOBALS['wp_actions']['woocommerce_load_cart_from_session'] );
+
+ add_filter(
+ 'woocommerce_get_cart_item_from_session',
+ static function () {
+ throw new \RuntimeException( 'Synthetic Store API cart-session failure.' );
+ }
+ );
+
+ $response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/cart' ) );
+
+ $this->assertSame( 500, $response->get_status(), 'A cart session failure should return a Store API error response.' );
+ $this->assertSame( 'woocommerce_rest_unknown_server_error', $response->get_data()['code'] );
+ }
+
+ /**
+ * @testdox Should return an error response when the cart session fails before it is restored.
+ */
+ public function test_cart_session_failure_before_restore_returns_error_response() {
+ // This filter runs before `get_cart_from_session()` fires its action, so nothing
+ // stops the response headers attempting a second load of the failed cart.
+ unset( $GLOBALS['wp_actions']['woocommerce_load_cart_from_session'] );
+
+ // A REST request never runs `initialize_cart()`, so the route starts with no
+ // cart at all. The test bootstrap leaves one behind.
+ $this->cart_backup = WC()->cart;
+ WC()->cart = null;
+
+ add_filter(
+ 'woocommerce_session_handler',
+ static function () {
+ throw new \RuntimeException( 'Synthetic session handler failure.' );
+ }
+ );
+
+ $response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/cart' ) );
+
+ $this->assertSame( 500, $response->get_status(), 'A cart session failure should return a Store API error response.' );
+ }
+
+ /**
+ * Restore the cart instance removed by the cart session failure tests.
+ */
+ public function tearDown(): void {
+ if ( $this->cart_backup instanceof \WC_Cart ) {
+ WC()->cart = $this->cart_backup;
+ $this->cart_backup = null;
+ }
+
+ parent::tearDown();
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
index f63c2b0f8e8..3065948c43e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
@@ -3208,4 +3208,27 @@ class Checkout extends \WP_Test_REST_TestCase {
$this->assertNull( $result, 'No payment method on a zero-total order should resolve to a null gateway.' );
$this->assertSame( 0, $gateway_resolution_count, 'Available payment gateways must not be resolved when no payment method is supplied.' );
}
+
+ /**
+ * @testdox Should return an error response when restoring the cart session throws.
+ */
+ public function test_cart_session_failure_returns_error_response() {
+ wc()->session->set( 'cart', wc()->cart->get_cart_for_session() );
+
+ // The route restores the cart only when this action has not run yet, so reset
+ // the counter to put the process back into the state a REST request starts in.
+ unset( $GLOBALS['wp_actions']['woocommerce_load_cart_from_session'] );
+
+ add_filter(
+ 'woocommerce_get_cart_item_from_session',
+ static function () {
+ throw new \RuntimeException( 'Synthetic Store API cart-session failure.' );
+ }
+ );
+
+ $response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/checkout' ) );
+
+ $this->assertSame( 500, $response->get_status(), 'A cart session failure should return a Store API error response.' );
+ $this->assertSame( 'woocommerce_rest_unknown_server_error', $response->get_data()['code'] );
+ }
}