Commit 1d9cd3211f9 for woocommerce

commit 1d9cd3211f999418032eb0d6ddaaa9f05a918869
Author: Miguel Gasca <miguel.gasca@automattic.com>
Date:   Thu Oct 15 16:45:35 2026 +0200

    Fix webhooks REST links skipping webhooks after an unaligned offset (#69832)

    * Fix webhooks REST links skipping webhooks after an unaligned offset

    #68499 made the webhooks list apply `offset` and built the prev and
    next links from the page the offset falls on. An offset that is not a
    multiple of per_page falls between two pages, so `next` skipped the
    rest of that page: per_page=2&offset=3 returned webhooks 4 and 5, and
    its next link returned 7. The first hop skips per_page minus
    (offset mod per_page) webhooks, and prev overlaps the current slice.

diff --git a/plugins/woocommerce/changelog/fix-WOOAIRR-514-webhooks-offset-links b/plugins/woocommerce/changelog/fix-WOOAIRR-514-webhooks-offset-links
new file mode 100644
index 00000000000..91af2be2462
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-WOOAIRR-514-webhooks-offset-links
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Fix webhooks REST pagination links for unaligned offsets from unreleased #68499; no merchant change.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php
index 1b61e5a1d84..80a908c33fe 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php
@@ -279,18 +279,30 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller {

 		$response       = rest_ensure_response( $webhooks );
 		$per_page       = (int) $prepared_args['limit'];
-		$page           = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
+		$offset         = (int) $prepared_args['offset'];
+		$page           = ceil( ( $offset / $per_page ) + 1 );
 		$total_webhooks = $results->total;
 		$max_pages      = $results->max_num_pages;
-		// The links below navigate by page, so the caller's offset must not ride along in
-		// them: it takes precedence over the page and would return this same slice again.
-		$link_params = $request->get_query_params();
-		unset( $link_params['offset'] );
+		$link_params    = $request->get_query_params();
+		unset( $link_params['offset'], $link_params['page'] );
 		$base = add_query_arg( $link_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

 		$response->header( 'X-WP-Total', $total_webhooks );
 		$response->header( 'X-WP-TotalPages', $max_pages );

+		// An offset that is not a multiple of per_page falls between two pages, so offset requests get offset links.
+		if ( ! empty( $request['offset'] ) ) {
+			$prev_end = min( $offset, $total_webhooks );
+			if ( $prev_end > 0 ) {
+				$response->link_header( 'prev', add_query_arg( 'offset', max( 0, $prev_end - $per_page ), $base ) );
+			}
+			if ( $offset + $per_page < $total_webhooks ) {
+				$response->link_header( 'next', add_query_arg( 'offset', $offset + $per_page, $base ) );
+			}
+
+			return $response;
+		}
+
 		if ( $page > 1 ) {
 			$prev_page = $page - 1;
 			if ( $prev_page > $max_pages ) {
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller-tests.php
index 3c284a376ee..1a742e74a54 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller-tests.php
@@ -325,24 +325,28 @@ class WC_REST_Webhooks_Controller_Tests extends WC_REST_Unit_Test_Case {
 	}

 	/**
-	 * @testdox The links an offset request advertises lead to the pages either side of it.
+	 * @testdox The links an offset request advertises lead to the webhooks either side of it, even when the offset is not a multiple of per_page.
+	 *
+	 * @testWith [2, 0, 4]
+	 *           [1, 0, 3]
+	 *
+	 * @param int $offset     The offset to request, over pages of 2.
+	 * @param int $prev_start Position of the first webhook the prev link should return.
+	 * @param int $next_start Position of the first webhook the next link should return.
 	 */
-	public function test_offset_pagination_links_lead_to_the_adjacent_pages(): void {
+	public function test_offset_pagination_links_lead_to_the_adjacent_pages( int $offset, int $prev_start, int $next_start ): void {
 		wp_set_current_user( 1 );
 		$webhook_ids = $this->create_webhooks( 5 );

 		$headers = $this->request_webhooks(
 			array(
 				'per_page' => 2,
-				'offset'   => 2,
+				'offset'   => $offset,
 			)
 		)->get_headers();

-		// Offset 2 over pages of 2 is page 2, so the links either side are pages 1 and 3.
-		// Following them has to move: an offset left in the link would win over its page
-		// and hand back the offset-2 slice again.
-		$this->assertSame( array_slice( $webhook_ids, 0, 2 ), $this->follow_link( $headers['Link'], 'prev' ) );
-		$this->assertSame( array_slice( $webhook_ids, 4, 2 ), $this->follow_link( $headers['Link'], 'next' ) );
+		$this->assertSame( array_slice( $webhook_ids, $prev_start, 2 ), $this->follow_link( $headers['Link'], 'prev' ) );
+		$this->assertSame( array_slice( $webhook_ids, $next_start, 2 ), $this->follow_link( $headers['Link'], 'next' ) );
 	}

 	/**