Repository navigation
Respect rate-limit errors by caching client errors when appropriate #13630
Description
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
- Exhaust the Analytics or Search Console API quota for a property (or force a
rateLimitExceeded/quotaExceededresponse from the Google API). - Load the Site Kit dashboard.
- See a report error with a "Retry" button.
- 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_Errorincludes acacheTTLvalue 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
cacheTTLis 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.phpsoexception_to_error()addscacheTTLto the error$datawhen the extractedreasonis a rate-limit or quota reason, using a named constant for the duration. - Update file
assets/js/util/errors.tsto add (or reuse) a rate-limit reason check and exclude such errors inisErrorRetryable(). - Verify
assets/js/components/ReportErrorActions.jsneeds no change onceisErrorRetryable()returnsfalse—if it renders a Retry button by another path, update it too.
Test Coverage
- Add PHPUnit coverage in
tests/phpunit/integration/Core/Modules/ModuleTest.phpasserting that aGoogle_Service_Exceptionwith each rate-limit reason produces aWP_Errorwith acacheTTLproperty, and that other reasons do not. - Update
assets/js/util/errors.test.tsassertingisErrorRetryable()returnsfalsefor each rate-limit reason and staystruefor an ordinary transient error. - Update
assets/js/googlesitekit/api/index.test.js(or add a case) asserting an error withcacheTTLis cached and a repeat request within the window does not hit the network. - Update the
ReportErrorActionstests/stories to cover the no-Retry rate-limit state, and add a VRT reference if the rendered state differs.
QA Brief
Setup
- Use Tweak extension in Chrome and open Site Kit > Dashboard once so the requests appear.
- Add a rule:
- Match URL contains:
/google-site-kit/v1/modules/analytics-4/data/report - Response status:
429 - Response body:
- Match URL contains:
{
"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
-
Every Analytics report shows its error with "Get help" and no "Retry" button.
-
The error message text is the same as before this change.
-
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. -
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.
-
Control case — an ordinary error must still be retryable. Change the rule to status
500with 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 noisErrorentry should appear in Session Storage. -
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
eugene-manuilov commentedon Sep 21, 2026
IB ✔️
mohitwp commentedon Oct 12, 2026
QA Update ✅
Tested on the
developbranch.Verified the following scenarios using the Tweak extension to mock Analytics API responses:
429 Rate-limit error:
isError: trueandttl: 600(10 minutes).500 Internal Server Error:
Details
Recording.3521.mp4