Skip to content

fix: retry read-only API requests the site rate-limits - #741

Merged
dcalhoun merged 7 commits into
trunkfrom
fix/retry-rate-limited-api-requests
Oct 3, 2026
Merged

dcalhoun merged 7 commits into
trunkfrom
fix/retry-rate-limited-api-requests

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

What?

Retry api-fetch requests with 429 status codes.

Why?

The editor sends numerous requests on load. Some hosts deny these requests with 429 responses. This disrupts the editor load. In particular, blocks like Categories List will stop functioning as Gutenberg's store will cache the response as an empty collection, which prevents the block from fetching the actual data later.

How?

Wrap @wordpress/api-fetch's default handler with retry logic. The logic targets safely repeatable requests with a backoff timing and jitter. It favors a Retry-After header value if present.

Note: this does not address these failures for the development server. The request in that environment are CORS and occur for preload OPTIONS requests. The status code is hidden from JavaScript in that context, so we cannot accurately detect the request should be retried.

Testing Instructions

Tip

This was primarily reproduced via WoW (Atomic) sites where the CDN often limits the batch of requests when the editor loads.

  1. Load the editor with a bundle (with GUTENBERG_EDITOR_URL unset)
  2. Inspect the editor after launch with Chrome's inspector (if needed, reload the editor page)
  3. Note any [GBK] Retrying... logs in the console for failed requests
  4. Insert the Categories List block
  5. Verify the block lists the sites categories, it is not empty/collapsed

Accessibility Testing Instructions

N/A, no presentation changes.

Screenshots or screencast

N/A, no presentation changes.


AI-generated details

What: Retries GET, HEAD, and OPTIONS requests that receive a 429, up to twice. It waits for Retry-After when present, but fails at once if it asks for more than 10s, since retries inside that window would also fail. Otherwise it waits 0.5–1s then 2–4s with jitter.

Why: Some hosts, such as WP Cloud sites, throttle the burst of REST requests sent while the editor loads. core-data caches a failed taxonomy entity config for the session, so the Categories List and Terms List blocks then never fetch terms until the editor reloads. #691 exposed this without adding requests: Gutenberg #78568 lazy-loads user pattern categories, which used to load the taxonomy config early, moving that load into the throttled part of the burst.

How: Wraps api-fetch's fetch handler rather than adding a middleware, so each network request is retried once, including fetchAllMiddleware pages. The wrapper requests the raw response to read its status and then parses it as api-fetch does, since api-fetch does not expose its parsing. Other statuses, network errors, offline, and aborted requests behave as before.

Limitation: With the Vite dev server, the editor is cross-origin, so a throttled request fails at the CORS preflight as a network error with no status. This PR does not cover that case.

Testing:

  1. Load the editor with a production build on a site that throttles (e.g. a WP Cloud staging site) until taxonomies?context=view returns 429.
  2. Confirm the console logs Retrying GET …/taxonomies?context=view… after a 429 response.
  3. Insert a Categories List block and confirm it lists the site's categories.

🤖 Generated with Claude Code

https://claude.ai/code/session_011A5docu5NJzK5jRH8X5AnS

@github-actions github-actions Bot added the [Type] Bug An existing feature does not function as intended label Sep 28, 2026
@wpmobilebot

wpmobilebot commented Sep 28, 2026 •

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/741")

Built from 275cedd

dcalhoun and others added 6 commits October 2, 2026 14:55
Some hosts, such as WP Cloud sites, throttle the burst of REST requests
sent while the editor loads with a 429. core-data caches a failed
taxonomy entity config for the session, so a throttled load leaves the
Categories List and Terms List blocks unable to fetch terms until the
editor reloads. The @WordPress bump in #691 exposed this: Gutenberg
#78568 moved that load later in the burst, where it is throttled.

Only readable 429 responses are retried, so the dev server, whose
throttled CORS preflights surface as network errors, is not covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011A5docu5NJzK5jRH8X5AnS
Clamping a long Retry-After to the cap sent both retries inside the
window the server had closed, so they could only fail. The request now
rejects immediately instead of after ~20s and two wasted requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y5SN26ekqCm2EmaR4CddU
Tests missed a jitter regression, the HTTP-date Retry-After form, and
drift between the wrapper's copied parsing and api-fetch's own. A
parity test now compares both handlers, and the no-retry tests fail on
call counts rather than timing out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y5SN26ekqCm2EmaR4CddU
Keep the why for the final design and drop implementation detail that
parseResponse already documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y5SN26ekqCm2EmaR4CddU
Trunk's @wordpress/eslint-plugin 27 bump (#738) rejects `*` types, which the
rate-limit retry's response parser used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A retried 429 is expected and self-healing, so it needs no attention. A 429
that exhausts its retries, or whose Retry-After exceeds the cap, can leave a
failure cached for the session, and was previously logged nowhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dcalhoun
dcalhoun force-pushed the fix/retry-rate-limited-api-requests branch from 487df9e to 263a96b Compare October 2, 2026 19:03
@dcalhoun
dcalhoun marked this pull request as ready for review October 2, 2026 19:07
@dcalhoun
dcalhoun requested review from nbradbury and oguzkocer and removed request for nbradbury October 2, 2026 19:13

@oguzkocer oguzkocer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left a minor suggestion, but otherwise looks good to me. :shipit:

Comment thread src/utils/api-fetch.js Outdated
// A retry sent before a longer `Retry-After` elapses would fail too.
if ( delay <= MAX_RETRY_AFTER_MS ) {
info( `Retrying ${ request } after a 429 response` );
await wait( delay );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this will keep waiting for the entire delay duration even if the signal is aborted during that time, and a listener for the abort event could reject early with signal.reason.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Addressed in 275cedd.

The retry delay ignored the request's signal, so an aborted request stayed
pending until the delay elapsed. It now rejects at once with the signal's
reason, as fetch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dcalhoun
dcalhoun enabled auto-merge (squash) October 3, 2026 11:58
@dcalhoun
dcalhoun merged commit 3e766c3 into trunk Oct 3, 2026
24 checks passed
@dcalhoun
dcalhoun deleted the fix/retry-rate-limited-api-requests branch October 3, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants