Remove DaemonConnectionException (#885)

* remove DaemonConnectionException

* update tests
This commit is contained in:
Boy132 2025-01-07 22:58:04 +01:00 committed by GitHub
parent 6fcf4173d3
commit c93a836ad8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 237 additions and 484 deletions

View File

@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Exception;
class BulkPowerActionCommand extends Command
{
@ -71,7 +71,7 @@ class BulkPowerActionCommand extends Command
try {
$powerRepository->setServer($server)->send($action);
} catch (DaemonConnectionException $exception) {
} catch (Exception $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name,
'id' => $server->id,

View File

@ -1,73 +0,0 @@
<?php
namespace App\Exceptions\Http\Connection;
use Exception;
use Illuminate\Http\Response;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
class DaemonConnectionException extends DisplayException
{
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
/**
* Every request to the daemon instance will return a unique X-Request-Id header
* which allows for all errors to be efficiently tied to a specific request that
* triggered them, and gives users a more direct method of informing hosts when
* something goes wrong.
*/
private ?string $requestId;
/**
* Throw a displayable exception caused by a daemon connection error.
*/
public function __construct(?Exception $previous, bool $useStatusCode = true)
{
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
$this->requestId = $response?->getHeaderLine('X-Request-Id');
Context::add('request_id', $this->requestId);
if ($useStatusCode) {
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
// There are rare conditions where daemon encounters a panic condition and crashes the
// request being made after content has already been sent over the wire. In these cases
// you can end up with a "successful" response code that is actual an error.
//
// Handle those better here since we shouldn't ever end up in this exception state and
// be returning a 2XX level response.
if ($this->statusCode < 400) {
$this->statusCode = Response::HTTP_BAD_GATEWAY;
}
}
if (is_null($response)) {
$message = 'Could not establish a connection to the machine running this server. Please try again.';
} else {
$message = sprintf('There was an error while communicating with the machine running this server. This error has been logged, please try again. (code: %s) (request_id: %s)', $response->getStatusCode(), $this->requestId ?? '<nil>');
}
// Attempt to pull the actual error message off the response and return that if it is not
// a 500 level error.
if ($this->statusCode < 500 && !is_null($response)) {
$body = json_decode($response->getBody()->__toString(), true);
$message = sprintf('An error occurred on the remote host: %s. (request id: %s)', $body['error'] ?? $message, $this->requestId ?? '<nil>');
}
$level = $this->statusCode >= 500 && $this->statusCode !== 504
? DisplayException::LEVEL_ERROR
: DisplayException::LEVEL_WARNING;
parent::__construct($message, $previous, $level);
}
/**
* Return the HTTP status code for this exception.
*/
public function getStatusCode(): int
{
return $this->statusCode;
}
}

View File

@ -3,10 +3,10 @@
namespace App\Filament\Server\Pages;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
@ -18,8 +18,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Alignment;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Number;
class Settings extends ServerFormPage
@ -202,17 +200,23 @@ class Settings extends ServerFormPage
->modalHeading('Are you sure you want to reinstall the server?')
->modalDescription('Some files may be deleted or modified during this process, please back up your data before continuing.')
->modalSubmitActionLabel('Yes, Reinstall')
->action(function (Server $server) {
->action(function (Server $server, DaemonServerRepository $serverRepository) {
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
$server->fill(['status' => ServerState::Installing])->save();
try {
Http::daemon($server->node)->post(sprintf(
'/api/servers/%s/reinstall',
$server->uuid
));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
$serverRepository->reinstall();
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title('Server Reinstall failed')
->body($exception->getMessage())
->send();
return;
}
Activity::event('server:settings.reinstall')
@ -220,7 +224,7 @@ class Settings extends ServerFormPage
Notification::make()
->success()
->title('Server Reinstall Started')
->title('Server Reinstall started')
->send();
}),
])

View File

@ -70,4 +70,12 @@ abstract class ApplicationApiController extends Controller
{
return new Response('', Response::HTTP_NO_CONTENT);
}
/**
* Return an HTTP/406 response for the API.
*/
protected function returnNotAcceptable(): Response
{
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
}

View File

@ -10,6 +10,7 @@ use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Response;
class ServerManagementController extends ApplicationApiController
@ -80,19 +81,19 @@ class ServerManagementController extends ApplicationApiController
}
// Node was not viable
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
return $this->returnNotAcceptable();
}
/**
* Cancels a transfer of a server to a new node.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
{
if (!$transfer = $server->transfer) {
// Server is not transferring
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
return $this->returnNotAcceptable();
}
$transfer->successful = true;

View File

@ -2,12 +2,15 @@
namespace App\Http\Controllers\Api\Application\Servers;
use App\Exceptions\Model\DataValidationException;
use App\Models\User;
use App\Models\Server;
use App\Services\Servers\StartupModificationService;
use App\Transformers\Api\Application\ServerTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Servers\UpdateServerStartupRequest;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Validation\ValidationException;
class StartupController extends ApplicationApiController
{
@ -22,9 +25,9 @@ class StartupController extends ApplicationApiController
/**
* Update the startup and environment settings for a specific server.
*
* @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws \App\Exceptions\Model\DataValidationException
* @throws ValidationException
* @throws ConnectionException
* @throws DataValidationException
*/
public function index(UpdateServerStartupRequest $request, Server $server): array
{

View File

@ -10,27 +10,25 @@ use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Http\Requests\Api\Client\Servers\SendCommandRequest;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Exception;
use Illuminate\Http\Client\ConnectionException;
class CommandController extends ClientApiController
{
/**
* Send a command to a running server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function index(SendCommandRequest $request, Server $server): Response
{
try {
$server->send($request->input('command'));
} catch (DaemonConnectionException $exception) {
} catch (Exception $exception) {
$previous = $exception->getPrevious();
if ($previous instanceof BadResponseException) {
if (
$previous->getResponse() instanceof ResponseInterface
&& $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY
) {
if ($previous->getResponse() instanceof ResponseInterface && $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY) {
throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception);
}
}
@ -38,7 +36,9 @@ class CommandController extends ClientApiController
throw $exception;
}
Activity::event('server:console.command')->property('command', $request->input('command'))->log();
Activity::event('server:console.command')
->property('command', $request->input('command'))
->log();
return $this->returnNoContent();
}

View File

@ -22,6 +22,7 @@ use App\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
use App\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
use App\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
use App\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
use Illuminate\Http\Client\ConnectionException;
class FileController extends ClientApiController
{
@ -38,7 +39,7 @@ class FileController extends ClientApiController
/**
* Returns a listing of files in a given directory.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function directory(ListFilesRequest $request, Server $server): array
{
@ -63,7 +64,9 @@ class FileController extends ClientApiController
config('panel.files.max_edit_size')
);
Activity::event('server:file.read')->property('file', $request->get('file'))->log();
Activity::event('server:file.read')
->property('file', $request->get('file'))
->log();
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
}
@ -102,13 +105,17 @@ class FileController extends ClientApiController
/**
* Writes the contents of the specified file to the server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
{
$this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
$this->fileRepository
->setServer($server)
->putContent($request->get('file'), $request->getContent());
Activity::event('server:file.write')->property('file', $request->get('file'))->log();
Activity::event('server:file.write')
->property('file', $request->get('file'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
@ -154,7 +161,7 @@ class FileController extends ClientApiController
/**
* Copies a file on the server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function copy(CopyFileRequest $request, Server $server): JsonResponse
{
@ -162,13 +169,15 @@ class FileController extends ClientApiController
->setServer($server)
->copyFile($request->input('location'));
Activity::event('server:file.copy')->property('file', $request->input('location'))->log();
Activity::event('server:file.copy')
->property('file', $request->input('location'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function compress(CompressFilesRequest $request, Server $server): array
{
@ -188,7 +197,7 @@ class FileController extends ClientApiController
}
/**
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
{
@ -210,7 +219,7 @@ class FileController extends ClientApiController
/**
* Deletes files or folders for the server in the given root directory.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
{
@ -230,7 +239,7 @@ class FileController extends ClientApiController
/**
* Updates file permissions for file(s) in the given root directory.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
{

View File

@ -9,6 +9,7 @@ use App\Transformers\Api\Client\StatsTransformer;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Http\Requests\Api\Client\Servers\GetServerRequest;
use Illuminate\Http\Client\ConnectionException;
class ResourceUtilizationController extends ClientApiController
{
@ -25,7 +26,7 @@ class ResourceUtilizationController extends ClientApiController
* 20 seconds at a time to ensure that repeated requests to this endpoint do not cause
* a flood of unnecessary API calls.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function __invoke(GetServerRequest $request, Server $server): array
{

View File

@ -11,7 +11,7 @@ use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class ServerTransferController extends Controller
{
@ -75,7 +75,7 @@ class ServerTransferController extends Controller
->setServer($server)
->setNode($transfer->oldNode)
->delete();
} catch (DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
logger()->warning($exception, ['transfer_id' => $server->transfer->id]);
}

View File

@ -11,8 +11,9 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs;
use App\Services\Backups\InitiateBackupService;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Services\Files\DeleteFilesService;
use Exception;
use Illuminate\Http\Client\ConnectionException;
class RunTaskJob extends Job implements ShouldQueue
{
@ -72,10 +73,10 @@ class RunTaskJob extends Job implements ShouldQueue
default:
throw new \InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
}
} catch (\Exception $exception) {
// If this isn't a DaemonConnectionException on a task that allows for failures
} catch (Exception $exception) {
// If this isn't a ConnectionException on a task that allows for failures
// throw the exception back up the chain so that the task is stopped.
if (!($this->task->continue_on_failure && $exception instanceof DaemonConnectionException)) {
if (!($this->task->continue_on_failure && $exception instanceof ConnectionException)) {
throw $exception;
}
}
@ -87,7 +88,7 @@ class RunTaskJob extends Job implements ShouldQueue
/**
* Handle a failure while sending the action to the daemon or otherwise processing the job.
*/
public function failed(?\Exception $exception = null): void
public function failed(): void
{
$this->markTaskNotQueued();
$this->markScheduleComplete();

View File

@ -310,7 +310,7 @@ class Node extends Model
// @phpstan-ignore-next-line
return resolve(DaemonConfigurationRepository::class)
->setNode($this)
->getSystemInformation(connectTimeout: 3);
->getSystemInformation();
} catch (Exception $exception) {
$message = str($exception->getMessage());

View File

@ -3,11 +3,8 @@
namespace App\Repositories\Daemon;
use Illuminate\Http\Client\Response;
use Webmozart\Assert\Assert;
use App\Models\Backup;
use App\Models\Server;
use GuzzleHttp\Exception\TransferException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class DaemonBackupRepository extends DaemonRepository
{
@ -26,64 +23,42 @@ class DaemonBackupRepository extends DaemonRepository
/**
* Tells the remote Daemon to begin generating a backup for the server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function backup(Backup $backup): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/backup', $this->server->uuid),
[
'adapter' => $this->adapter ?? config('backups.default'),
'uuid' => $backup->uuid,
'ignore' => implode("\n", $backup->ignored_files),
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/backup",
[
'adapter' => $this->adapter ?? config('backups.default'),
'uuid' => $backup->uuid,
'ignore' => implode("\n", $backup->ignored_files),
]
);
}
/**
* Sends a request to daemon to begin restoring a backup for a server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function restore(Backup $backup, ?string $url = null, bool $truncate = false): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/backup/%s/restore', $this->server->uuid, $backup->uuid),
[
'adapter' => $backup->disk,
'truncate_directory' => $truncate,
'download_url' => $url ?? '',
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/backup/$backup->uuid/restore",
[
'adapter' => $backup->disk,
'truncate_directory' => $truncate,
'download_url' => $url ?? '',
]
);
}
/**
* Deletes a backup from the daemon.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function delete(Backup $backup): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->delete(
sprintf('/api/servers/%s/backup/%s', $this->server->uuid, $backup->uuid)
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->delete("/api/servers/{$this->server->uuid}/backup/$backup->uuid");
}
}

View File

@ -3,8 +3,7 @@
namespace App\Repositories\Daemon;
use App\Models\Node;
use GuzzleHttp\Exception\TransferException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
class DaemonConfigurationRepository extends DaemonRepository
@ -12,20 +11,13 @@ class DaemonConfigurationRepository extends DaemonRepository
/**
* Returns system information from the daemon instance.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function getSystemInformation(?int $version = null, int $connectTimeout = 5): array
public function getSystemInformation(): array
{
try {
$response = $this
->getHttpClient()
->connectTimeout($connectTimeout)
->get('/api/system' . (!is_null($version) ? '?v=' . $version : ''));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $response->json() ?? [];
return $this->getHttpClient()
->connectTimeout(3)
->get('/api/system')->throw()->json();
}
/**
@ -33,17 +25,10 @@ class DaemonConfigurationRepository extends DaemonRepository
* this instance using a passed-in model. This allows us to change plenty of information
* in the model, and still use the old, pre-update model to actually make the HTTP request.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function update(Node $node): Response
{
try {
return $this->getHttpClient()->post(
'/api/update',
$node->getConfiguration(),
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post('/api/update', $node->getConfiguration());
}
}

View File

@ -2,15 +2,10 @@
namespace App\Repositories\Daemon;
use Carbon\CarbonInterval;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Client\Response;
use Webmozart\Assert\Assert;
use App\Models\Server;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\TransferException;
use App\Exceptions\Http\Server\FileSizeTooLargeException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class DaemonFileRepository extends DaemonRepository
{
@ -19,23 +14,16 @@ class DaemonFileRepository extends DaemonRepository
*
* @param int|null $notLargerThan the maximum content length in bytes
*
* @throws \GuzzleHttp\Exception\TransferException
* @throws \App\Exceptions\Http\Server\FileSizeTooLargeException
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
* @throws FileSizeTooLargeException
* @throws ConnectionException
* @throws FileNotFoundException
*/
public function getContent(string $path, ?int $notLargerThan = null): string
{
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/contents', $this->server->uuid),
['file' => $path]
);
} catch (ClientException|TransferException $exception) {
throw new DaemonConnectionException($exception);
}
$response = $this->getHttpClient()->get("/api/servers/{$this->server->uuid}/files/contents",
['file' => $path]
);
$length = $response->header('Content-Length');
if ($notLargerThan && $length > $notLargerThan) {
@ -53,215 +41,145 @@ class DaemonFileRepository extends DaemonRepository
* Save new contents to a given file. This works for both creating and updating
* a file.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function putContent(string $path, string $content): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()
->withQueryParameters(['file' => $path])
->withBody($content)
->post(sprintf('/api/servers/%s/files/write', $this->server->uuid));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()
->withQueryParameters(['file' => $path])
->withBody($content)
->post("/api/servers/{$this->server->uuid}/files/write");
}
/**
* Return a directory listing for a given path.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function getDirectory(string $path): array
{
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/list-directory', $this->server->uuid),
['directory' => $path]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $response->json();
return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}/files/list-directory",
['directory' => $path]
)->json();
}
/**
* Creates a new directory for the server in the given $path.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function createDirectory(string $name, string $path): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
[
'name' => $name,
'path' => $path,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/create-directory",
[
'name' => $name,
'path' => $path,
]
);
}
/**
* Renames or moves a file on the remote machine.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function renameFiles(?string $root, array $files): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->put(
sprintf('/api/servers/%s/files/rename', $this->server->uuid),
[
'root' => $root ?? '/',
'files' => $files,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->put("/api/servers/{$this->server->uuid}/files/rename",
[
'root' => $root ?? '/',
'files' => $files,
]
);
}
/**
* Copy a given file and give it a unique name.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function copyFile(string $location): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
[
'location' => $location,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/copy",
['location' => $location]
);
}
/**
* Delete a file or folder for the server.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function deleteFiles(?string $root, array $files): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
[
'root' => $root ?? '/',
'files' => $files,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/delete",
[
'root' => $root ?? '/',
'files' => $files,
]
);
}
/**
* Compress the given files or folders in the given root.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function compressFiles(?string $root, array $files): array
{
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
->timeout(60 * 15)
->post(
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
[
'root' => $root ?? '/',
'files' => $files,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $response->json();
return $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
->timeout(60 * 15)
->post("/api/servers/{$this->server->uuid}/files/compress",
[
'root' => $root ?? '/',
'files' => $files,
]
)->json();
}
/**
* Decompresses a given archive file.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function decompressFile(?string $root, string $file): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()
// Wait for up to 15 minutes for the decompress to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
->timeout((int) CarbonInterval::minutes(15)->totalSeconds)
->post(
sprintf('/api/servers/%s/files/decompress', $this->server->uuid),
[
'root' => $root ?? '/',
'file' => $file,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
->timeout(60 * 15)
->post("/api/servers/{$this->server->uuid}/files/decompress",
[
'root' => $root ?? '/',
'file' => $file,
]
);
}
/**
* Chmods the given files.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function chmodFiles(?string $root, array $files): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/chmod', $this->server->uuid),
[
'root' => $root ?? '/',
'files' => $files,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/chmod",
[
'root' => $root ?? '/',
'files' => $files,
]
);
}
/**
* Pulls a file from the given URL and saves it to the disk.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function pull(string $url, ?string $directory, array $params = []): Response
{
Assert::isInstanceOf($this->server, Server::class);
$attributes = [
'url' => $url,
'root' => $directory ?? '/',
@ -270,39 +188,25 @@ class DaemonFileRepository extends DaemonRepository
'foreground' => $params['foreground'] ?? null,
];
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/pull', $this->server->uuid),
array_filter($attributes, fn ($value) => !is_null($value))
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/pull", array_filter($attributes, fn ($value) => !is_null($value)));
}
/**
* Searches all files in the directory (and its subdirectories) for the given search term.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function search(string $searchTerm, ?string $directory): array
{
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()
->timeout(120)
->get(
sprintf('/api/servers/%s/files/search', $this->server->uuid),
[
'pattern' => $searchTerm,
'directory' => $directory ?? '/',
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $response->json();
return $this->getHttpClient()
// Wait for up to 2 minutes for the search to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
->timeout(60 * 2)
->get("/api/servers/{$this->server->uuid}/files/search",
[
'pattern' => $searchTerm,
'directory' => $directory ?? '/',
]
)->json();
}
}

View File

@ -2,30 +2,20 @@
namespace App\Repositories\Daemon;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Webmozart\Assert\Assert;
use App\Models\Server;
use GuzzleHttp\Exception\TransferException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonPowerRepository extends DaemonRepository
{
/**
* Sends a power action to the server instance.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function send(string $action): Response
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/power', $this->server->uuid),
['action' => $action],
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power",
['action' => $action],
);
}
}

View File

@ -17,9 +17,7 @@ class DaemonServerRepository extends DaemonRepository
public function getDetails(): array
{
try {
return $this->getHttpClient()->get(
sprintf('/api/servers/%s', $this->server->uuid)
)->throw()->json();
return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}")->throw()->json();
} catch (RequestException $exception) {
$cfId = $exception->response->header('Cf-Ray');
$cfCache = $exception->response->header('Cf-Cache-Status');

View File

@ -11,7 +11,7 @@ use Illuminate\Database\ConnectionInterface;
use App\Extensions\Backups\BackupManager;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Exceptions\Service\Backup\BackupLockedException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class DeleteBackupService
{
@ -48,8 +48,9 @@ class DeleteBackupService
$this->connection->transaction(function () use ($backup) {
try {
$this->daemonBackupRepository->setServer($backup->server)->delete($backup);
} catch (DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
$previous = $exception->getPrevious();
// Don't fail the request if the Daemon responds with a 404, just assume the backup
// doesn't actually exist and remove its reference from the Panel as well.
if (!$previous instanceof ClientException || $previous->getResponse()->getStatusCode() !== Response::HTTP_NOT_FOUND) {

View File

@ -2,9 +2,9 @@
namespace App\Services\Files;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Str;
class DeleteFilesService
@ -19,7 +19,7 @@ class DeleteFilesService
/**
* Deletes the given files.
*
* @throws DaemonConnectionException
* @throws ConnectionException
*/
public function handle(Server $server, array $files): void
{

View File

@ -6,8 +6,8 @@ use Illuminate\Support\Str;
use App\Models\Node;
use Illuminate\Database\ConnectionInterface;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Exceptions\Service\Node\ConfigurationNotPersistedException;
use Illuminate\Http\Client\ConnectionException;
class NodeUpdateService
{
@ -42,7 +42,7 @@ class NodeUpdateService
$node->fqdn = $updated->fqdn;
$this->configurationRepository->setNode($node)->update($updated);
} catch (DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
logger()->warning($exception, ['node_id' => $node->id]);
// Never actually throw these exceptions up the stack. If we were able to change the settings

View File

@ -9,7 +9,6 @@ use App\Jobs\Schedule\RunTaskJob;
use Illuminate\Database\ConnectionInterface;
use App\Exceptions\DisplayException;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
class ProcessScheduleService
{
@ -53,13 +52,7 @@ class ProcessScheduleService
return;
}
} catch (\Exception $exception) {
if (!$exception instanceof DaemonConnectionException) {
// If we encountered some exception during this process that wasn't just an
// issue connecting to daemon run the failed sequence for a job. Otherwise we
// can just quietly mark the task as completed without actually running anything.
$job->failed($exception);
}
} catch (Exception) {
$job->failed();
return;
@ -73,8 +66,8 @@ class ProcessScheduleService
// so we need to manually trigger it and then continue with the exception throw.
try {
$this->dispatcher->dispatchNow($job);
} catch (\Exception $exception) {
$job->failed($exception);
} catch (Exception $exception) {
$job->failed();
throw $exception;
}

View File

@ -8,7 +8,7 @@ use App\Models\Allocation;
use Illuminate\Database\ConnectionInterface;
use App\Exceptions\DisplayException;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class BuildModificationService
{
@ -64,7 +64,7 @@ class BuildModificationService
if (!empty($updateData['build'])) {
try {
$this->daemonServerRepository->setServer($server)->sync();
} catch (DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
logger()->warning($exception, ['server_id' => $server->id]);
}
}

View File

@ -7,7 +7,6 @@ use App\Models\Server;
use Illuminate\Database\ConnectionInterface;
use App\Traits\Services\ReturnsUpdatedModels;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class DetailsModificationService
@ -42,7 +41,7 @@ class DetailsModificationService
if ($server->owner_id !== $owner) {
try {
$this->serverRepository->setServer($server)->revokeUserJTI($owner);
} catch (ConnectionException|DaemonConnectionException) {
} catch (ConnectionException) {
// Do nothing. A failure here is not ideal, but it is likely to be caused by daemon
// being offline, or in an entirely broken state. Remember, these tokens reset every
// few minutes by default, we're just trying to help it along a little quicker.

View File

@ -8,7 +8,7 @@ use App\Models\Server;
use Illuminate\Database\ConnectionInterface;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Databases\DatabaseManagementService;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class ServerDeletionService
{
@ -43,12 +43,12 @@ class ServerDeletionService
{
try {
$this->daemonServerRepository->setServer($server)->delete();
} catch (DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
// If there is an error not caused a 404 error and this isn't a forced delete,
// go ahead and bail out. We specifically ignore a 404 since that can be assumed
// to be a safe error, meaning the server doesn't exist at all on daemon so there
// is no reason we need to bail out from that.
if (!$this->force && $exception->getStatusCode() !== Response::HTTP_NOT_FOUND) {
if (!$this->force && $exception->getCode() !== Response::HTTP_NOT_FOUND) {
throw $exception;
}
@ -61,7 +61,7 @@ class ServerDeletionService
foreach ($server->databases as $database) {
try {
$this->databaseManagementService->delete($database);
} catch (\Exception $exception) {
} catch (Exception $exception) {
if (!$this->force) {
throw $exception;
}

View File

@ -7,7 +7,6 @@ use App\Enums\SuspendAction;
use Filament\Notifications\Notification;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Doctrine\DBAL\Exception\ConnectionException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
class SuspensionService
@ -47,11 +46,7 @@ class SuspensionService
'status' => $isSuspending ? ServerState::Suspended : null,
]);
try {
// Tell daemon to re-sync the server state.
$this->daemonServerRepository->setServer($server)->sync();
} catch (ConnectionException $exception) {
throw $exception;
}
// Tell daemon to re-sync the server state.
$this->daemonServerRepository->setServer($server)->sync();
}
}

View File

@ -2,14 +2,12 @@
namespace App\Services\Servers;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Allocation;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerTransfer;
use App\Services\Nodes\NodeJWTService;
use Carbon\CarbonImmutable;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Token\Plain;
@ -26,21 +24,17 @@ class TransferServerService
private function notify(Server $server, Plain $token): void
{
try {
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
])->toPsrResponse();
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
],
])->toPsrResponse();
}
/**

View File

@ -3,7 +3,6 @@
namespace App\Services\Subusers;
use App\Events\Server\SubUserRemoved;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Facades\Activity;
use App\Models\Server;
use App\Models\Subuser;
@ -30,7 +29,7 @@ class SubuserDeletionService
try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
} catch (ConnectionException|DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);

View File

@ -2,7 +2,6 @@
namespace App\Services\Subusers;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Facades\Activity;
use App\Models\Server;
use App\Models\Subuser;
@ -39,7 +38,7 @@ class SubuserUpdateService
try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
} catch (ConnectionException|DaemonConnectionException $exception) {
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance. Chances are it is
// offline and the token will be invalid once daemon boots back.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);

View File

@ -10,8 +10,8 @@ use App\Models\Server;
use App\Models\Permission;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
use Illuminate\Http\Client\ConnectionException;
use Symfony\Component\HttpKernel\Exception\HttpException;
class CommandControllerTest extends ClientApiIntegrationTestCase
@ -77,11 +77,7 @@ class CommandControllerTest extends ClientApiIntegrationTestCase
[$user, $server] = $this->generateTestAccount();
$server = \Mockery::mock($server)->makePartial();
$server->expects('send')->andThrows(
new DaemonConnectionException(
new BadResponseException('', new Request('GET', 'test'), new GuzzleResponse(Response::HTTP_BAD_GATEWAY))
)
);
$server->expects('send')->andThrows(new ConnectionException(previous: new BadResponseException('', new Request('GET', 'test'), new GuzzleResponse(Response::HTTP_BAD_GATEWAY))));
$this->instance(Server::class, $server);

View File

@ -5,17 +5,14 @@ namespace App\Tests\Integration\Jobs\Schedule;
use App\Enums\ServerState;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use GuzzleHttp\Psr7\Request;
use App\Models\Task;
use GuzzleHttp\Psr7\Response;
use App\Models\Server;
use App\Models\Schedule;
use Illuminate\Support\Facades\Bus;
use App\Jobs\Schedule\RunTaskJob;
use GuzzleHttp\Exception\BadResponseException;
use App\Tests\Integration\IntegrationTestCase;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class RunTaskJobTest extends IntegrationTestCase
{
@ -126,12 +123,10 @@ class RunTaskJobTest extends IntegrationTestCase
$mock = \Mockery::mock(DaemonPowerRepository::class);
$this->instance(DaemonPowerRepository::class, $mock);
$mock->expects('setServer->send')->andThrow(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response()))
);
$mock->expects('setServer->send')->andThrow(new ConnectionException());
if (!$continueOnFailure) {
$this->expectException(DaemonConnectionException::class);
$this->expectException(ConnectionException::class);
}
Bus::dispatchSync(new RunTaskJob($task));

View File

@ -12,7 +12,7 @@ use App\Services\Backups\DeleteBackupService;
use App\Tests\Integration\IntegrationTestCase;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Exceptions\Service\Backup\BackupLockedException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class DeleteBackupServiceTest extends IntegrationTestCase
{
@ -54,11 +54,7 @@ class DeleteBackupServiceTest extends IntegrationTestCase
$backup = Backup::factory()->create(['server_id' => $server->id]);
$mock = $this->mock(DaemonBackupRepository::class);
$mock->expects('setServer->delete')->with($backup)->andThrow(
new DaemonConnectionException(
new ClientException('', new Request('DELETE', '/'), new Response(404))
)
);
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(404))));
$this->app->make(DeleteBackupService::class)->handle($backup);
@ -73,13 +69,9 @@ class DeleteBackupServiceTest extends IntegrationTestCase
$backup = Backup::factory()->create(['server_id' => $server->id]);
$mock = $this->mock(DaemonBackupRepository::class);
$mock->expects('setServer->delete')->with($backup)->andThrow(
new DaemonConnectionException(
new ClientException('', new Request('DELETE', '/'), new Response(500))
)
);
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(500))));
$this->expectException(DaemonConnectionException::class);
$this->expectException(ConnectionException::class);
$this->app->make(DeleteBackupService::class)->handle($backup);

View File

@ -3,16 +3,13 @@
namespace App\Tests\Integration\Services\Servers;
use Mockery\MockInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use App\Models\Server;
use App\Models\Allocation;
use GuzzleHttp\Exception\RequestException;
use App\Exceptions\DisplayException;
use App\Tests\Integration\IntegrationTestCase;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\BuildModificationService;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class BuildModificationServiceTest extends IntegrationTestCase
{
@ -149,11 +146,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->sync')->andThrows(
new DaemonConnectionException(
new RequestException('Bad request', new Request('GET', '/test'), new Response())
)
);
$this->daemonServerRepository->expects('setServer->sync')->andThrows(new ConnectionException());
$response = $this->getService()->handle($server, ['memory' => 256, 'disk' => 10240]);

View File

@ -3,16 +3,13 @@
namespace App\Tests\Integration\Services\Servers;
use Mockery\MockInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use App\Models\Database;
use App\Models\DatabaseHost;
use GuzzleHttp\Exception\BadResponseException;
use App\Tests\Integration\IntegrationTestCase;
use App\Services\Servers\ServerDeletionService;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Databases\DatabaseManagementService;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use Illuminate\Http\Client\ConnectionException;
class ServerDeletionServiceTest extends IntegrationTestCase
{
@ -59,11 +56,9 @@ class ServerDeletionServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$this->expectException(DaemonConnectionException::class);
$this->expectException(ConnectionException::class);
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response()))
);
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(new ConnectionException());
$this->getService()->handle($server);
@ -77,9 +72,7 @@ class ServerDeletionServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(404)))
);
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(new ConnectionException(code: 404));
$this->getService()->handle($server);
@ -94,9 +87,7 @@ class ServerDeletionServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(
new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(500)))
);
$this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows(new ConnectionException());
$this->getService()->withForce()->handle($server);