Skip to content

fix(site-health): live bounded API probe; collapse timeout consts to two#582

Merged
galbus merged 4 commits into
mainfrom
claude/hopeful-jemison-05ab59
Jul 19, 2026
Merged

fix(site-health): live bounded API probe; collapse timeout consts to two#582
galbus merged 4 commits into
mainfrom
claude/hopeful-jemison-05ab59

Conversation

@galbus

@galbus galbus commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Two related changes to how the plugin bounds blocking requests:

1. Site Health probe: bounded and gated, but live

Site Health > Info (SiteHealth::add_rest_api_connection()) issued a blocking wp_remote_request() to the BeyondWords API on every render — with no timeout override (WordPress' 5s default), even on fresh installs with no credentials.

  • Skip entirely when unconfiguredUtils::has_api_creds() gate; fresh installs make zero HTTP requests and show "Not checked — BeyondWords API credentials are not configured."
  • Bound the request to the shared 3s ceiling (was an unbounded 5s default).
  • Fixed a latent DNS bug — the error path passed the full URL to gethostbyname() (a no-op). resolve_api_host() parses the host first so the "Unable to reach … at {IP}" diagnostic reports a real IP. Core passes a bare hostname here too.

Deliberately NOT cached. An earlier revision cached the result in a transient; that was reverted after checking prior art: WordPress core runs this same probe uncached on every Info render (WP_Debug_Data::get_wp_core()wp_remote_get('https://wordpress.org', ['timeout' => 10])). Site Health is a diagnostic — a cached result keeps reporting "unreachable" to an admin who just fixed creds/DNS and hit refresh. No phpcs rule requires caching (RemoteRequestTimeout only errors above 3s; LowExpiryCacheTime only inspects wp_cache_*). vip_safe_wp_remote_get() is also avoided: its circuit breaker reports failure without retrying, which would make the diagnostic lie. Same for Client::call_api(), which clears beyondwords_valid_api_connection on 401 as a side effect.

2. Timeout constants: six names for three values → two constants

Per current guidance, the only genuinely slow BeyondWords endpoint is the voices GET — content create/update returns the content ID immediately and generates audio asynchronously server-side. So the generous 30s default had no remaining justification, and the pass-through aliases added churn:

Before After
Client::REQUEST_TIMEOUT = 30 (default) removed — default is now 3s
Client::DELETE_TIMEOUT = 3 removed — uses the default
Client::RENDER_TIMEOUT = 3 removed — uses the default
Client::VOICES_TIMEOUT = 8 Client::VOICES_REQUEST_TIMEOUT = 8
Utils::CONNECTION_CHECK_TIMEOUT = 3 removed — uses the default
Client::DEFAULT_REQUEST_TIMEOUT = 3 (default of call_api()/build_args()/cached_get())

Behaviour change (intentional): content create/read/update, the Magic Embed player bootstrap, and batch delete drop from an implicit 30s timeout to 3s.

Testing

  • npm run phpcs (WordPress-VIP-Go): clean, no phpcs:ignore added.
  • PHPUnit ClientTest + SyncTest + UtilsTest + SiteHealthTest: 167 tests / 456 assertions passing. New/updated cases assert: the probe is not cached (two renders ⇒ two requests), zero requests without credentials, the probe and all requests use DEFAULT_REQUEST_TIMEOUT (≤ 3s per VIP), and voices keeps its longer bound. The old "write paths keep the longer timeout" test premise is replaced by all_requests_use_the_short_default_timeout.
  • Full Cypress suite not run. The site-health spec's asserted string "BeyondWords API is reachable" is unchanged.

🤖 Generated with Claude Code

Site Health > Info issued a synchronous wp_remote_request() to the
BeyondWords API on every render, with no timeout override (WP's 5s
default), no caching, and even on fresh installs with no credentials.
A slow or unreachable API blocked the admin page for up to 5s per view.

- Skip the probe entirely when Utils::has_api_creds() is false; show
  a "credentials not configured" status instead.
- Cache the result in a 5-minute transient so repeat views and "Copy
  site info" do not re-hit the network.
- Bound the request: prefer vip_safe_wp_remote_get() where available,
  else wp_remote_request() with an explicit 2s timeout (under VIP's 3s
  ceiling).
- Resolve the host before gethostbyname() so the unreachable message
  reports a real IP (it was passing the full URL, a no-op).

Adds PHPUnit coverage for the configured, unconfigured-skip, cached,
and transport-error paths. No phpcs:ignore added; phpcs clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

galbus and others added 2 commits July 19, 2026 09:23
Reverses the caching approach taken earlier in this branch. WordPress
core does this same probe uncached on every Info-screen render
(WP_Debug_Data::get_wp_core() hits wordpress.org with a 10s timeout),
because Site Health is a diagnostic: a cached result keeps reporting
"unreachable" to an admin who just fixed creds/DNS and hit refresh.

- Drop the 5-minute transient so every render re-probes. No lint rule
  required it: RemoteRequestTimeout only errors above 3s, and
  LowExpiryCacheTime only applies to wp_cache_*, so phpcs stays green.
- Use the 3s bound already established on main rather than 2s, which
  risked a false "unreachable" on a slow-but-working link.
- Drop the vip_safe_wp_remote_get branch: its circuit breaker reports a
  failure without retrying, which would make the diagnostic lie.

Centralise the 3s value as Client::BLOCKING_TIMEOUT, the single place to
retune the VIP blocking-path ceiling. DELETE_TIMEOUT, RENDER_TIMEOUT and
Utils::CONNECTION_CHECK_TIMEOUT now alias it while keeping their own
rationale. Every call site and test already used the named constants, so
this is non-breaking.

Credential-gating and the gethostbyname() host fix are unchanged; core
passes a bare hostname there too, which the plugin previously got wrong.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@galbus galbus changed the title perf(site-health): cache + bound the REST API reachability probe fix(site-health): bound the REST API probe and share one 3s timeout const Jul 19, 2026
There were six timeout constants for three values (3, 8, 30), several of
them pass-through aliases. Per current guidance the only genuinely slow
BeyondWords endpoint is the voices GET — content create/update returns
the content ID immediately and generates audio asynchronously
server-side — so the generous 30-second default no longer has a
justification. Collapse to:

- Client::DEFAULT_REQUEST_TIMEOUT = 3, the default for call_api(),
  build_args() and cached_get() (VIP's blocking-request ceiling)
- Client::VOICES_REQUEST_TIMEOUT = 8, the one override

Removed: REQUEST_TIMEOUT (30), BLOCKING_TIMEOUT, DELETE_TIMEOUT,
RENDER_TIMEOUT, VOICES_TIMEOUT, Utils::CONNECTION_CHECK_TIMEOUT.
delete_audio_by_ids() and the settings connection check now just use
the default; the Site Health probe references the shared constant.

Behaviour change: content create/read/update, the Magic Embed player
bootstrap, and batch delete drop from an implicit 30s timeout to 3s.

Tests updated to the new names; the render-vs-write-path test premise
("write paths keep the longer timeout") is replaced by
all_requests_use_the_short_default_timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@galbus galbus changed the title fix(site-health): bound the REST API probe and share one 3s timeout const fix(site-health): live bounded API probe; collapse timeout consts to two Jul 19, 2026
@galbus
galbus marked this pull request as ready for review July 19, 2026 09:14
@galbus
galbus requested a review from gouravkhunger July 19, 2026 09:15
@galbus

galbus commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@gouravkhunger there are multiple open and recently closed PRs without request timeout values.

This one includes the update to only use two values:

  • 3sec default for all requests, as per VIP guidelines
  • 8sec or voices query - the only override

If approved, I'll merge the changes here into other open PRs before we review those.

@galbus
galbus merged commit 322777b into main Jul 19, 2026
12 checks passed
@galbus
galbus deleted the claude/hopeful-jemison-05ab59 branch July 19, 2026 09:41
galbus added a commit that referenced this pull request Jul 19, 2026
Main's #582 collapsed the timeout constants: REQUEST_TIMEOUT (30s),
DELETE_TIMEOUT and RENDER_TIMEOUT are gone, replaced by
Client::DEFAULT_REQUEST_TIMEOUT (3s), with VOICES_REQUEST_TIMEOUT (8s)
the only exception.

Git auto-merged this textually but the result was semantically broken:
create_audio(), update_audio(), generate_audio_for_post() and
update_or_recreate_audio() all still defaulted their $timeout parameter
to the deleted Client::REQUEST_TIMEOUT, which fatals at call time, and a
SyncTest assertion referenced it too.

BULK_GENERATE_TIMEOUT (15s) existed only to pull the bulk loop below the
old generous 30s default. With the default now 3s it would instead make
the bulk path five times more permissive than every other call — the
opposite of this branch's intent — so it is removed entirely, along with
the $timeout parameter it was threaded through. Those four signatures
return to matching main, and the bulk loop inherits the 3s default.

BULK_GENERATE_SYNC_LIMIT stays: it bounds the number of blocking calls,
which is the actual N+1 fix and is orthogonal to per-call duration. The
shorter default makes it strictly better — worst case drops from
10 x 15s = 150s to 10 x 3s = 30s, inside a typical execution limit. The
SyncTest assertion is rewritten to guard exactly that invariant
(cap x per-call timeout <= 60s) instead of the removed constant.

Verified: full phpcs ruleset clean; full PHPUnit suite passes
(703 tests, 2240 assertions, 1 multisite-only skip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants