Real JSON null for empty product feature; SSR warm-cache button and CLI
Ticket 0068-FE: ProductFeatureJsonRenderer returned json_encode(null), which headless passes on as the STRING null (it only decodes object/array-looking strings) and crashed the front end. It now returns an empty string and the TypoScript field sets ifEmptyReturnNull = 1, giving a real JSON null. No other renderer used the pattern. Ticket 0066-FE: SsrPurgeService::warm() posts to the node /__warm endpoint (derived from SSR_PURGE_ENDPOINT, same secret), exposed as a Warm cache button with confirmation in the VITEC SSR module, a Cache warming progress row fed by /__health, and the CLI vitec:ssr-warm [paths]. Warm runs are deliberately not written to the purge log. Verified: two-path run 2/2 done, affected page delivers productfeature null. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb53ff67ee
commit
8f8ae039d4
7 files changed
+176
-3
No files matched your search
@@ -76,6 +76,79 @@ final class SsrPurgeService
|
||||
return max(1, (int)($this->getConfig()['SSR_FAILOVER_AFTER'] ?? 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /__warm on the node (contract 0066-FE), derived from
|
||||
* SSR_PURGE_ENDPOINT; SSR_WARM_ENDPOINT overrides explicitly.
|
||||
*/
|
||||
public function getWarmEndpoint(): string
|
||||
{
|
||||
$explicit = $this->getConfig()['SSR_WARM_ENDPOINT'] ?? '';
|
||||
if ($explicit !== '') {
|
||||
return $explicit;
|
||||
}
|
||||
$purge = $this->getConfig()['SSR_PURGE_ENDPOINT'] ?? '';
|
||||
return $purge === '' ? '' : str_replace('/__purge', '/__warm', $purge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a background warm run on the node: no paths (or '*') = every
|
||||
* page of sitemap.xml, otherwise exactly the given paths. The node
|
||||
* answers at once and renders in the background; progress shows up as
|
||||
* "warming" in /__health and /__stats. Not written to the purge log -
|
||||
* a warm run evicts nothing, and the log feeds the purge statistics.
|
||||
*
|
||||
* @param string[] $paths
|
||||
* @return array{success:bool, httpCode:int, message:string, data:array}
|
||||
*/
|
||||
public function warm(array $paths = []): array
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
return ['success' => false, 'httpCode' => 0, 'message' => 'not configured (var/vitec-ssr-purge.env missing)', 'data' => []];
|
||||
}
|
||||
$paths = $this->normalize($paths);
|
||||
$payload = ($paths === [] || $paths === ['*']) ? ['source' => 'sitemap'] : ['paths' => $paths];
|
||||
try {
|
||||
$response = $this->requestFactory->request($this->getWarmEndpoint(), 'POST', [
|
||||
'headers' => [
|
||||
'X-Purge-Secret' => $this->getConfig()['SSR_PURGE_SECRET'],
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => json_encode($payload, JSON_THROW_ON_ERROR),
|
||||
// The node reads the sitemap (index + sub-sitemaps) before
|
||||
// it answers; the pages themselves are not awaited.
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 3,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
$code = $response->getStatusCode();
|
||||
$body = (string)$response->getBody();
|
||||
$data = json_decode($body, true);
|
||||
$data = is_array($data) ? $data : [];
|
||||
if ($code === 200 && !empty($data['started'])) {
|
||||
$message = sprintf(
|
||||
'Warm run started: %d pages queued (source: %s, %d at a time)',
|
||||
(int)($data['queued'] ?? 0),
|
||||
(string)($data['source'] ?? '?'),
|
||||
(int)($data['concurrency'] ?? 0)
|
||||
);
|
||||
return ['success' => true, 'httpCode' => $code, 'message' => $message, 'data' => $data];
|
||||
}
|
||||
if ($code === 200 && !empty($data['running'])) {
|
||||
$w = (array)($data['warming'] ?? []);
|
||||
$message = sprintf(
|
||||
'A warm run is already active: %d of %d pages done',
|
||||
(int)($w['done'] ?? 0) + (int)($w['skipped'] ?? 0) + (int)($w['failed'] ?? 0),
|
||||
(int)($w['queued'] ?? 0)
|
||||
);
|
||||
return ['success' => true, 'httpCode' => $code, 'message' => $message, 'data' => $data];
|
||||
}
|
||||
$message = 'HTTP ' . $code . ' - ' . (string)($data['error'] ?? substr($body, 0, 200));
|
||||
return ['success' => false, 'httpCode' => $code, 'message' => $message, 'data' => $data];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'httpCode' => 0, 'message' => substr($e->getMessage(), 0, 200), 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes, dedupes and sends the paths; logs the outcome.
|
||||
* Never throws - editors' saves must not fail on a purge problem.
|
||||
|
||||
Reference in new issue
Block a user