fix(sentinel): surface remote fetch errors instead of a TypeError

Let instant_remote_process throw on failed SSH/docker commands so the
real error (e.g. a missing coolify-sentinel container or an unreadable
SSH key) is reported. A null result, such as a literal "null" body, is
turned into an empty string, which guard() rejects as an invalid response.

Run curl with -sS so progress output stays out of the output while
errors are still reported.

Add tests for a failed docker exec and a literal null body.
This commit is contained in:
Andras Bacsai
2026-09-28 08:27:47 +02:00
parent 8dee2f4cb5
commit 1c3de6a4cd
2 changed files with 24 additions and 11 deletions
+5 -11
View File
@@ -729,11 +729,9 @@ class SentinelTrafficClient
{
$token = $this->server->settings->ensureValidSentinelToken();
return instant_remote_process(
[$this->buildFetchCommand($token, $url)],
$this->server,
false
);
// Throw the real SSH/docker error (for example, an unreadable SSH key or a missing
// container). A literal "null" body comes back as null; guard() rejects the empty string.
return instant_remote_process([$this->buildFetchCommand($token, $url)], $this->server) ?? '';
}
/**
@@ -743,11 +741,7 @@ class SentinelTrafficClient
{
$token = $this->server->settings->ensureValidSentinelToken();
return instant_remote_process(
[$this->buildBatchCommand($token, $urls)],
$this->server,
false
);
return instant_remote_process([$this->buildBatchCommand($token, $urls)], $this->server) ?? '';
}
/**
@@ -762,7 +756,7 @@ class SentinelTrafficClient
*/
protected function buildFetchCommand(string $token, string $url): string
{
return "docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$token}\" \"{$url}\"'";
return "docker exec coolify-sentinel sh -c 'curl -sS -H \"Authorization: Bearer {$token}\" \"{$url}\"'";
}
/**
@@ -1,10 +1,12 @@
<?php
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use App\Services\SentinelTrafficClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Process;
uses(RefreshDatabase::class);
@@ -29,3 +31,20 @@ it('caches identical requests within the TTL', function () {
$client->overview('app', 'a', 'b');
expect($client->calls)->toBe(1);
});
it('surfaces the real remote error instead of a TypeError when the fetch fails', function (int $exitCode, string $output, string $errorOutput, string $message) {
Cache::flush();
config()->set('constants.ssh.max_retries', 1);
Process::fake(fn () => Process::result(output: $output, errorOutput: $errorOutput, exitCode: $exitCode));
$team = Team::factory()->create();
$server = Server::factory()->create([
'team_id' => $team->id,
'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id,
]);
expect(fn () => (new SentinelTrafficClient($server))->overview('app', 'a', 'b'))
->toThrow(RuntimeException::class, $message);
})->with([
'failed docker exec' => [1, '', 'Error response from daemon: No such container: coolify-sentinel', 'No such container: coolify-sentinel'],
'literal null body' => [0, 'null', '', 'Traffic analytics returned an invalid response.'],
]);