fix(templates): keep local service templates when the CDN bundle is unusable

A 200 response with an empty body or an error page replaced the local
templates and the shared cache, so the service list became empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-09-28 00:18:09 +02:00
1 parent 69eeb2f628
commit 1e66c9f9bd
3 files changed
+41 -2

No files matched your search

+5 -1
View File
@@ -34,7 +34,11 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
->get(config('constants.services.official'));
if ($response->successful()) {
// Shared cache so Cloud HTTP nodes see the same bundle Horizon pulled.
store_service_templates_bundle($response->body());
if (! store_service_templates_bundle($response->body())) {
Log::error('PullTemplatesFromCDN received an invalid bundle', [
'body' => str($response->body())->limit(500)->toString(),
]);
}
} else {
Log::error('PullTemplatesFromCDN failed', [
'status' => $response->status(),
+9 -1
View File
@@ -1484,6 +1484,12 @@ function service_templates_path(): string
*/
function store_service_templates_bundle(string $json, ?string $fetchedAt = null): bool
{
// A 200 response can still be an empty body or an error page; keep the current templates then.
$templates = json_decode($json, true);
if (! is_array($templates) || $templates === [] || array_is_list($templates)) {
return false;
}
$fetchedAt ??= now()->toIso8601String();
$path = service_templates_path();
@@ -1530,7 +1536,9 @@ function get_service_templates(bool $force = false): Collection
if ($response->failed()) {
return collect([]);
}
store_service_templates_bundle($response->body());
if (! store_service_templates_bundle($response->body())) {
return get_service_templates();
}
return collect(json_decode($response->body()))->sortKeys();
} catch (Throwable) {
@@ -119,6 +119,33 @@ it('skips pulling templates in local development', function () {
expect(Cache::get(service_templates_cache_key()))->toBeNull();
});
it('keeps the local templates when the CDN returns an unusable bundle', function (string $body) {
Http::fake([
config('constants.services.official') => Http::response($body, 200),
]);
$path = service_templates_path();
$original = File::get($path);
try {
config(['app.env' => 'production']);
(new PullTemplatesFromCDN)->handle();
expect(md5(File::get($path)))->toBe(md5($original))
->and(Cache::get(service_templates_cache_key()))->toBeNull()
->and(get_service_templates())->not->toBeEmpty();
} finally {
File::put($path, $original);
Cache::forget(service_templates_cache_key());
}
})->with([
'empty body' => '',
'html error page' => '<html>Bad gateway</html>',
'empty object' => '{}',
'json list' => '[1, 2]',
]);
it('logs when the CDN responds with a non-success status', function () {
Http::fake([
'cdn.coollabs.io/*' => Http::response('nope', 503),