Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/api/class-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ class Client {
*/
const RENDER_TIMEOUT = 3;

/**
* Timeout, in seconds, for the voices GET.
*
* Voices is the one editor dropdown that is genuinely slow to prepare —
* ~3.7s p95 in Sentry, against ~250ms p95 for every other editor GET — so
* the shared `RENDER_TIMEOUT` would abandon more than 5% of cold-cache
* fetches. Because a failure is then negative-cached, that would blank the
* Voice dropdown for a whole `CACHE_TTL_ON_ERROR` rather than just the one
* render. Still well under the generous `REQUEST_TIMEOUT` default, and only
* ever paid on a cache miss.
*
* @since 7.0.0
*/
const VOICES_TIMEOUT = 8;

/**
* Register WordPress hooks.
*
Expand Down Expand Up @@ -378,6 +393,9 @@ public static function get_languages(): array|null|false {
/**
* GET /organization/voices?filter[language.code]=…
*
* Passes the longer `VOICES_TIMEOUT` — this endpoint is materially slower
* than the other editor dropdowns.
*
* @param int|string $language_code BeyondWords language code (or numeric ID).
*
* @return array<mixed>|null|false
Expand All @@ -389,7 +407,7 @@ public static function get_voices( int|string $language_code ): array|null|false
rawurlencode( strval( $language_code ) )
);

return self::cached_get( 'voices_' . $language_code, $url );
return self::cached_get( 'voices_' . $language_code, $url, self::VOICES_TIMEOUT );
}

/**
Expand Down Expand Up @@ -596,29 +614,32 @@ private static function cache_key( string $suffix ): string {
* (WP_Error / timeout, 5xx, 4xx, non-JSON) are negative-cached for the much
* shorter {@see CACHE_TTL_ON_ERROR}, so a slow or unreachable API is probed
* at most once per interval instead of re-issuing a blocking request on
* every admin render. The request itself uses the short
* {@see RENDER_TIMEOUT} for the same reason.
* every admin render. The request itself defaults to the short
* {@see RENDER_TIMEOUT} for the same reason; the slower voices endpoint
* passes its own {@see VOICES_TIMEOUT}.
*
* A 401 additionally self-heals: call_api() clears
* `beyondwords_valid_api_connection`, which ungates the metabox next load.
*
* @since 7.0.0
*
* @param string $suffix Cache-key suffix (include any project/language id).
* @param string $url Absolute endpoint URL.
* @param string $suffix Cache-key suffix (include any project/language id).
* @param string $url Absolute endpoint URL.
* @param int $timeout Request timeout in seconds. Defaults to RENDER_TIMEOUT;
* the voices GET passes the longer VOICES_TIMEOUT.
*
* @return array<mixed>|null|false Decoded body on the fetching call; the cached
* value ([] after a cached failure) thereafter.
*/
private static function cached_get( string $suffix, string $url ): array|null|false {
private static function cached_get( string $suffix, string $url, int $timeout = self::RENDER_TIMEOUT ): array|null|false {
$key = self::cache_key( $suffix );
$cached = get_transient( $key );

if ( false !== $cached ) {
return $cached;
}

$response = self::call_api( 'GET', $url, '', false, [], self::RENDER_TIMEOUT );
$response = self::call_api( 'GET', $url, '', false, [], $timeout );
$decoded = json_decode( wp_remote_retrieve_body( $response ), true );

if (
Expand Down
43 changes: 43 additions & 0 deletions tests/phpunit/api/test-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,49 @@ public function editor_dropdown_responses_are_cached()
delete_option('beyondwords_project_id');
}

/**
* @test
* @group settings
*
* Voices is materially slower to prepare than the other dropdowns (~3.7s p95
* vs ~250ms), so it gets its own longer bound — the shared RENDER_TIMEOUT
* would abandon cold-cache fetches and, because failures are negative-cached,
* blank the Voice dropdown for a whole CACHE_TTL_ON_ERROR.
*/
public function voices_get_uses_longer_timeout()
{
update_option('beyondwords_api_key', BEYONDWORDS_TESTS_API_KEY);
update_option('beyondwords_project_id', BEYONDWORDS_TESTS_PROJECT_ID);

$captured = null;
$filter = function ($preempt, $args, $url) use (&$captured) {
if (str_contains((string) $url, '/organization/voices')) {
$captured = $args['timeout'] ?? null;
}
return $preempt; // Pass through — let the mock supply the response.
};
add_filter('pre_http_request', $filter, 0, 3);

Client::get_voices('en_US');

remove_filter('pre_http_request', $filter, 0);

$this->assertSame(Client::VOICES_TIMEOUT, $captured);
$this->assertGreaterThan(
Client::RENDER_TIMEOUT,
Client::VOICES_TIMEOUT,
'Voices needs a longer bound than the other editor GETs'
);
$this->assertLessThan(
Client::REQUEST_TIMEOUT,
Client::VOICES_TIMEOUT,
'Voices still runs during page generation, so it must stay well under the default'
);

delete_option('beyondwords_api_key');
delete_option('beyondwords_project_id');
}

/**
* @test
* @group settings
Expand Down
Loading