Commit 7da69e839aa for woocommerce
commit 7da69e839aa742fd7dc983cafbe023fa4f6fcbae
Author: Sergey Mitroshin <sergeymitr@gmail.com>
Date: Mon Nov 2 12:50:38 2026 -0500
Remove excessive headers from UCP responses (#70450)
Remove the Store API headers we don't need from UCP responses.
diff --git a/plugins/woocommerce/changelog/fix-ucp-remove-cart-header b/plugins/woocommerce/changelog/fix-ucp-remove-cart-header
new file mode 100644
index 00000000000..3ce34f68783
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-ucp-remove-cart-header
@@ -0,0 +1,3 @@
+Significance: patch
+Type: fix
+Comment: Send only a Cache-Control header on the unreleased UCP cart routes, dropping the Store API browser headers.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
index 70a613ece41..605a7a7718a 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Ucp/AbstractCartRoute.php
@@ -81,6 +81,24 @@ abstract class AbstractCartRoute extends StoreApiCartRoute {
}
}
+ /**
+ * Send only the cache directive; the other Store API headers serve browser clients.
+ *
+ * A UCP client holds the cart credential as the path id and reads `id` from the body,
+ * sends no nonce and keeps no hash-keyed cache. Leaving the parent's builder out also
+ * keeps a `Cart-Token` for whatever session is in memory off an answer built before
+ * the route binds one, such as the not-found refusal above.
+ *
+ * @param \WP_REST_Response $response The response object.
+ *
+ * @return \WP_REST_Response
+ */
+ protected function add_response_headers( \WP_REST_Response $response ) {
+ $response->header( 'Cache-Control', 'no-store' );
+
+ return $response;
+ }
+
/**
* Put the request's cart token slot back the way it was found.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/UcpCart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/UcpCart.php
index 2659c0a934d..24400f0755e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/UcpCart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/UcpCart.php
@@ -806,16 +806,7 @@ class UcpCart extends ControllerTestCase {
* A cart an extension preloaded is not written by a UCP create: it mints its own session.
*/
public function test_preloaded_cookie_session_is_not_written_by_create(): void {
- // Stand in for an extension that loads the cart on `init`: a cookie session is in
- // memory and the "loaded" marker is set, so the route's handler swap would be a no-op
- // unless the route discards it first.
- WC()->initialize_session();
- WC()->initialize_cart();
- WC()->cart->add_to_cart( self::$product_ids[0], 1 );
- $GLOBALS['wp_actions']['woocommerce_load_cart_from_session'] = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Simulates an extension that already loaded the cart.
-
- $this->assertInstanceOf( \WC_Session_Handler::class, WC()->session );
- $cookie_customer = WC()->session->get_customer_id();
+ $cookie_customer = $this->preload_cookie_cart();
$response = $this->request( 'POST', '/carts', array( 'line_items' => array( $this->line_item( 1, 5 ) ) ) );
$data = $response->get_data();
@@ -829,6 +820,49 @@ class UcpCart extends ControllerTestCase {
$this->assertNotSame( $cookie_customer, $written_customer, 'A create must not write the preloaded cookie session.' );
}
+ /**
+ * A UCP response carries the cache directive and none of the Store API browser headers.
+ *
+ * The parent's builder re-mints a `Cart-Token` for whatever session is in memory, so on
+ * a not-found answer, built before the route binds a session, it would sign the cookie
+ * session of the shopper whose browser the request rode in on.
+ */
+ public function test_responses_carry_only_the_cache_directive(): void {
+ $expected = array( 'Cache-Control' => 'no-store' );
+
+ $this->preload_cookie_cart();
+
+ foreach ( array( array( 'GET', '' ), array( 'PUT', '' ), array( 'POST', '/cancel' ) ) as list( $method, $suffix ) ) {
+ $response = $this->request( $method, '/carts/not-a-real-token' . $suffix );
+
+ $this->assertSame( 'not_found', $response->get_data()['messages'][0]['code'], $method . $suffix );
+ $this->assertSame( $expected, $response->get_headers(), $method . $suffix . ' must not sign the preloaded session.' );
+ }
+
+ $created = $this->request( 'POST', '/carts', array( 'line_items' => array( $this->line_item( 1, 1 ) ) ) );
+ $this->assertSame( 'success', $created->get_data()['ucp']['status'] );
+ $this->assertSame( $expected, $created->get_headers(), 'create' );
+ }
+
+ /**
+ * Stand in for an extension that loads the cart on `init`.
+ *
+ * Leaves a cookie session with one line in memory and the "loaded" marker set, so a
+ * route's handler swap would be a no-op unless the route discards it first.
+ *
+ * @return string The preloaded session's customer id.
+ */
+ private function preload_cookie_cart(): string {
+ WC()->initialize_session();
+ WC()->initialize_cart();
+ WC()->cart->add_to_cart( self::$product_ids[0], 1 );
+ $GLOBALS['wp_actions']['woocommerce_load_cart_from_session'] = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Simulates an extension that already loaded the cart.
+
+ $this->assertInstanceOf( \WC_Session_Handler::class, WC()->session );
+
+ return (string) WC()->session->get_customer_id();
+ }
+
/**
* Cancel answers with the cart as it stood, then the id stops resolving.
*/