Skip to content

Respect rate-limit errors by caching client errors when appropriate #13630

Closed

Description

@tofumatt

Bug Description

When a Google API request fails because the quota is exhausted, isErrorRetryable() currently treats it as retryable. (Right now, isErrorRetryable() only excludes insufficient-permissions errors, permission-scope errors, and auth errors. Eg. ReportErrorActions offers the user a "Retry" button that goes straight back into an exhausted quota, fails again, and offers Retry again.

This is wrong today across the whole plugin, but it matters more with Fresh Data: those reports refresh on a five-minute cache window rather than an hour, so quota errors become marginally more likely.

siteKitRequest checks error?.data?.cacheTTL and, when present, caches the error for that long. Nothing server-side sends cacheTTL in the error data though.

Emitting cacheTTL from Module::exception_to_error() for rate-limit responses means the client suppresses the immediate retry on its own.

Steps to reproduce

  1. Exhaust the Analytics or Search Console API quota for a property (or force a rateLimitExceeded / quotaExceeded response from the Google API).
  2. Load the Site Kit dashboard.
  3. See a report error with a "Retry" button.
  4. Click Retry: the same quota error returns immediately, and "Retry" is offered again.

Additional Context

  • Applies to every module that reports through Module::exception_to_error(), not only Analytics.

Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance criteria

  • When a Google API request fails with a rate-limit or quota-exceeded reason, the resulting WP_Error includes a cacheTTL value in its error data.
    • The cacheTTL should be either the amount of time the Google API told us to wait, or a sane limit like 10 minutes. During implementation, check Google APIs for usual API rate limit cooldown periods.
  • The client should cache that error for the given TTL, so repeating the same request within the window does not re-issue it to the Google API. (Should already happen when cacheTTL is sent from the server.
  • A rate-limit error is not presented to the user as retryable.
  • Errors that are not rate-limit errors are unchanged, and stay retryable where they are/aren't today.
  • The error message shown to the user is unchanged; only the retry behaviour and caching change.

Implementation Brief

Module::exception_to_error() (includes/Core/Modules/Module.php, around line 676) already extracts reason from a Google_Service_Exception and builds a $data array containing status and reason. Add cacheTTL to that array when the reason indicates a rate limit. The Google API reasons to cover are rateLimitExceeded, userRateLimitExceeded, and quotaExceeded: check assets/js/util/errors.ts for the reason strings the client already recognises and keep the two lists in agreement.

On the client, isErrorRetryable() in assets/js/util/errors.ts needs a rate-limit exclusion alongside the existing isInsufficientPermissionsError / isPermissionScopeError / isAuthError checks. There is likely already a helper for detecting these reasons—reuse it rather than adding a second list.

Nothing else is needed on the client: siteKitRequest in assets/js/googlesitekit/api/index.js already reads error?.data?.cacheTTL and caches the error for that long.

Pick the TTL to match the Google APIs' quota windows rather than guessing — Analytics Data API quotas reset hourly and daily — and document the choice in a comment next to the constant.

  • Update file includes/Core/Modules/Module.php so exception_to_error() adds cacheTTL to the error $data when the extracted reason is a rate-limit or quota reason, using a named constant for the duration.
  • Update file assets/js/util/errors.ts to add (or reuse) a rate-limit reason check and exclude such errors in isErrorRetryable().
  • Verify assets/js/components/ReportErrorActions.js needs no change once isErrorRetryable() returns false—if it renders a Retry button by another path, update it too.

Test Coverage

  • Add PHPUnit coverage in tests/phpunit/integration/Core/Modules/ModuleTest.php asserting that a Google_Service_Exception with each rate-limit reason produces a WP_Error with a cacheTTL property, and that other reasons do not.
  • Update assets/js/util/errors.test.ts asserting isErrorRetryable() returns false for each rate-limit reason and stays true for an ordinary transient error.
  • Update assets/js/googlesitekit/api/index.test.js (or add a case) asserting an error with cacheTTL is cached and a repeat request within the window does not hit the network.
  • Update the ReportErrorActions tests/stories to cover the no-Retry rate-limit state, and add a VRT reference if the rendered state differs.

QA Brief

Setup

  1. Use Tweak extension in Chrome and open Site Kit > Dashboard once so the requests appear.
  2. Add a rule:
    • Match URL contains: /google-site-kit/v1/modules/analytics-4/data/report
    • Response status: 429
    • Response body:
         {
           "code": 429,
           "message": "Exhausted property tokens for a project per hour.",
           "data": { "status": 429, "reason": "", "cacheTTL": 600 }
         }

This is the exact payload the plugin now produces for a real Analytics quota error. Note reason is empty on purpose - the Analytics API doesn't send one, so the status is what identifies the rate limit.
3. Enable the rule and reload the dashboard.

Checks

  1. Every Analytics report shows its error with "Get help" and no "Retry" button.

  2. The error message text is the same as before this change.

  3. The error was stored rather than discarded. Open DevTools > Application > Session Storage > the site's origin, and filter the keys by analytics-4::report. At least one entry should hold: "isError": true, "ttl": 600. That entry is what stops the same request going back out to Google inside the window.

  4. Disable the rule (leave the page open, don't reload) and use the dashboard normally. The Analytics reports keep showing the error rather than silently succeeding - the stored error is being used instead of a new request.

  5. Control case — an ordinary error must still be retryable. Change the rule to status 500 with this body, clear Session Storage, then open the dashboard:
    { "code": "internal_server_error", "message": "Internal server error", "data": { "status": 500 } }
    The "Retry" button must be present, clicking it must issue new requests (Network tab), and no isError entry should appear in Session Storage.

  6. Disable the rule, clear Session Storage, reload, and confirm the dashboard is back to normal.

Known behaviour

A full page reload re-issues the report requests even while the stored error is still inside its 600-second window. That is pre-existing Site Kit cache behaviour, not something this change introduces: successful responses behave the same way, which you can see by watching a Search Console request refire while its successful response sits in Session Storage. Within a page session the stored error is used; the missing Retry button is what protects the reader across reloads.

Changelog entry

  • Improve handling of rate limit Google API errors.

Activity

  1. eugene-manuilov commented on Sep 21, 2026

    @eugene-manuilov
    Collaborator

    IB ✔️

  2. removed their assignment
    on Sep 21, 2026
  3. assigned and unassigned on Sep 21, 2026
  4. self-assigned this
    on Sep 24, 2026
  5. removed their assignment
    on Oct 6, 2026
  6. self-assigned this
    on Oct 7, 2026
  7. mohitwp commented on Oct 12, 2026

    @mohitwp
    Collaborator

    QA Update ✅

    • Tested on the develop branch.

    • Verified the following scenarios using the Tweak extension to mock Analytics API responses:

    • 429 Rate-limit error:

      • Confirmed the Analytics error message is displayed correctly.
      • Verified the "Get help" link is available and the "Retry" button is not displayed.
      • Confirmed the error is cached in Session Storage with isError: true and ttl: 600 (10 minutes).
      • Disabled the Tweak rule without reloading the page and verified that the cached error remained visible.
      • Confirmed no additional Analytics report requests were triggered when navigating between dashboard sections.
    • 500 Internal Server Error:

      • Verified the error message is displayed with the "Retry" button and "Get help" link.
      • Confirmed clicking "Retry" triggers new API requests.
      • Verified that the 500 error is not cached in Session Storage.
      • Disabled the Tweak rule and confirmed Analytics data loads successfully when retrying.
    Details Image
    Recording.3521.mp4
  8. removed their assignment
    on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Medium priorityTeam SIssues for Squad 1Type: EnhancementImprovement of an existing feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions