diff --git a/app/config/errors.php b/app/config/errors.php deleted file mode 100644 index 62492a9..0000000 --- a/app/config/errors.php +++ /dev/null @@ -1,90 +0,0 @@ - [ - 'name' => Exception::GENERAL_UNKNOWN, - 'short' => 'Whoops', - 'message' => 'Internal server error.', - 'code' => 500, - ], - Exception::GENERAL_ROUTE_NOT_FOUND => [ - 'name' => Exception::GENERAL_ROUTE_NOT_FOUND, - 'short' => 'Not found', - 'message' => 'The requested route was not found.', - 'code' => 404, - ], - Exception::GENERAL_UNAUTHORIZED => [ - 'name' => Exception::GENERAL_UNAUTHORIZED, - 'short' => 'Unauthorized', - 'message' => 'You are not authorized to access this resource.', - 'code' => 401, - ], - /* Runtime */ - Exception::RUNTIME_FAILED => [ - 'name' => Exception::RUNTIME_FAILED, - 'short' => 'Failed', - 'message' => 'Runtime failed.', - 'code' => 400, - 'publish' => false, - ], - Exception::RUNTIME_TIMEOUT => [ - 'name' => Exception::RUNTIME_TIMEOUT, - 'short' => 'Timeout', - 'message' => 'Timed out waiting for runtime.', - 'code' => 400, - ], - Exception::RUNTIME_NOT_FOUND => [ - 'name' => Exception::RUNTIME_CONFLICT, - 'short' => 'Not found', - 'message' => 'Runtime not found', - 'code' => 404, - ], - Exception::RUNTIME_CONFLICT => [ - 'name' => Exception::RUNTIME_CONFLICT, - 'short' => 'Conflict', - 'message' => 'Runtime already exists ', - 'code' => 409, - ], - /* Execution */ - Exception::EXECUTION_BAD_REQUEST => [ - 'name' => Exception::EXECUTION_BAD_REQUEST, - 'short' => 'Invalid request', - 'message' => 'Execution request was invalid.', - 'code' => 400, - ], - Exception::EXECUTION_BAD_JSON => [ - 'name' => Exception::EXECUTION_BAD_JSON, - 'short' => 'Invalid response', - 'message' => 'Execution resulted in binary response, but JSON response does not allow binaries. Use "Accept: multipart/form-data" header to support binaries.', - 'code' => 400, - ], - Exception::EXECUTION_TIMEOUT => [ - 'name' => Exception::EXECUTION_TIMEOUT, - 'short' => 'Timeout', - 'message' => 'Timed out waiting for execution.', - 'code' => 400, - ], - /* Logs */ - Exception::LOGS_TIMEOUT => [ - 'name' => Exception::LOGS_TIMEOUT, - 'short' => 'Timeout', - 'message' => 'Timed out waiting for logs.', - 'code' => 504, - ], - /* Command */ - Exception::COMMAND_TIMEOUT => [ - 'name' => Exception::COMMAND_TIMEOUT, - 'short' => 'Timeout', - 'message' => 'Operation timed out.', - 'code' => 500, - ], - Exception::COMMAND_FAILED => [ - 'name' => Exception::COMMAND_FAILED, - 'short' => 'Failed', - 'message' => 'Failed to execute command.', - 'code' => 500, - ], -]; diff --git a/app/controllers.php b/app/controllers.php index 4571c55..25042e5 100644 --- a/app/controllers.php +++ b/app/controllers.php @@ -2,7 +2,10 @@ require_once __DIR__ . '/init.php'; -use OpenRuntimes\Executor\Exception; +use OpenRuntimes\Executor\ExecutionBadJsonException; +use OpenRuntimes\Executor\ExecutionBadRequestException; +use OpenRuntimes\Executor\GeneralRouteNotFoundException; +use OpenRuntimes\Executor\GeneralUnauthorizedException; use OpenRuntimes\Executor\BodyMultipart; use OpenRuntimes\Executor\Runner\Adapter as Runner; use Utopia\System\System; @@ -199,13 +202,13 @@ function ( // 'headers' validator $validator = new Assoc(); if (!$validator->isValid($headers)) { - throw new Exception(Exception::EXECUTION_BAD_REQUEST, $validator->getDescription()); + throw new ExecutionBadRequestException($validator->getDescription()); } // 'variables' validator $validator = new Assoc(); if (!$validator->isValid($variables)) { - throw new Exception(Exception::EXECUTION_BAD_REQUEST, $validator->getDescription()); + throw new ExecutionBadRequestException($validator->getDescription()); } if (in_array($payload, [null, '', '0'], true)) { @@ -256,7 +259,7 @@ function ( if ($isJson) { $executionString = \json_encode($execution, JSON_UNESCAPED_UNICODE); if (!$executionString) { - throw new Exception(Exception::EXECUTION_BAD_JSON); + throw new ExecutionBadJsonException(); } $response @@ -286,12 +289,17 @@ function ( $response->setStatusCode(Response::STATUS_CODE_OK)->text("OK"); }); +Http::wildcard() + ->action(function (): void { + throw new GeneralRouteNotFoundException(); + }); + Http::init() ->groups(['api']) ->inject('request') ->action(function (Request $request): void { $secretKey = \explode(' ', $request->getHeader('authorization', ''))[1] ?? ''; if ($secretKey === '' || $secretKey === '0' || $secretKey !== System::getEnv('OPR_EXECUTOR_SECRET', '')) { - throw new Exception(Exception::GENERAL_UNAUTHORIZED, 'Missing executor key'); + throw new GeneralUnauthorizedException('Missing executor key'); } }); diff --git a/app/error.php b/app/error.php index de07fd5..91a76a7 100644 --- a/app/error.php +++ b/app/error.php @@ -1,6 +1,6 @@ inject('error') ->inject('response') ->action(function (Throwable $error, Response $response): void { - // Show all Executor\Exceptions, or everything if in development - $public = $error instanceof Exception || Http::isDevelopment(); - $exception = $public ? $error : new Exception(Exception::GENERAL_UNKNOWN); - $code = $exception->getCode() ?: 500; + if ($error instanceof HttpException) { + if ($error->publish) { + // TODO: publish to logger/Sentry + } - $output = [ - 'type' => $exception instanceof Exception ? $exception->getType() : Exception::GENERAL_UNKNOWN, - 'message' => $exception->getMessage(), - 'code' => $code, - 'version' => System::getEnv('OPR_EXECUTOR_VERSION', 'unknown') - ]; + $code = $error->statusCode; + $output = [ + 'type' => $error->type, + 'message' => $error->getMessage(), + 'code' => $code, + 'version' => System::getEnv('OPR_EXECUTOR_VERSION', 'unknown'), + ]; + } else { + // TODO: always publish to logger/Sentry + $code = 500; + $output = [ + 'type' => 'general_unknown', + 'message' => Http::isDevelopment() ? $error->getMessage() : 'Internal server error.', + 'code' => 500, + 'version' => System::getEnv('OPR_EXECUTOR_VERSION', 'unknown'), + ]; + } - // If in development, include some additional details. if (Http::isDevelopment()) { - $output['file'] = $exception->getFile(); - $output['line'] = $exception->getLine(); - $output['trace'] = \json_encode($exception->getTrace(), JSON_UNESCAPED_UNICODE) === false ? [] : $exception->getTrace(); + $output['file'] = $error->getFile(); + $output['line'] = $error->getLine(); + $output['trace'] = \json_encode($error->getTrace(), JSON_UNESCAPED_UNICODE) === false ? [] : $error->getTrace(); } $response diff --git a/app/init.php b/app/init.php index 9a80880..22fe518 100644 --- a/app/init.php +++ b/app/init.php @@ -10,13 +10,10 @@ use Utopia\Orchestration\Adapter\DockerAPI; use Utopia\Orchestration\Orchestration; use Utopia\System\System; -use Utopia\Config\Config; const MAX_LOG_SIZE = 5 * 1024 * 1024; const MAX_BUILD_LOG_SIZE = 1000 * 1000; -Config::load('errors', __DIR__ . '/config/errors.php'); - $container = new Container(); $container->set( diff --git a/src/Executor/CommandFailedException.php b/src/Executor/CommandFailedException.php new file mode 100644 index 0000000..37ba8bc --- /dev/null +++ b/src/Executor/CommandFailedException.php @@ -0,0 +1,11 @@ +_ - */ - public const string GENERAL_UNKNOWN = 'general_unknown'; - - public const string GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; - - public const string GENERAL_UNAUTHORIZED = 'general_unauthorized'; - - public const string EXECUTION_BAD_REQUEST = 'execution_bad_request'; - - public const string EXECUTION_TIMEOUT = 'execution_timeout'; - - public const string EXECUTION_BAD_JSON = 'execution_bad_json'; - - public const string RUNTIME_NOT_FOUND = 'runtime_not_found'; - - public const string RUNTIME_CONFLICT = 'runtime_conflict'; - - public const string RUNTIME_FAILED = 'runtime_failed'; - - public const string RUNTIME_TIMEOUT = 'runtime_timeout'; - - public const string LOGS_TIMEOUT = 'logs_timeout'; - - public const string COMMAND_TIMEOUT = 'command_timeout'; - - public const string COMMAND_FAILED = 'command_failed'; - - protected readonly string $short; - - protected readonly bool $publish; - - /** - * Constructor for the Exception class. - * - * @param string $type The type of exception. This will automatically set fallbacks for the other parameters. - * @param string|null $message The error message. - * @param int|null $code The error code. - * @param \Throwable|null $previous The previous exception. - */ - public function __construct( - protected readonly string $type = Exception::GENERAL_UNKNOWN, - ?string $message = null, - ?int $code = null, - ?\Throwable $previous = null - ) { - $errors = Config::getParam('errors'); - $error = $errors[$this->type] ?? []; - - $this->message = $message ?? $error['message']; - $this->code = $code ?? $error['code'] ?: 500; - $this->short = $error['short'] ?? ''; - - $this->publish = $error['publish'] ?? true; - - parent::__construct($this->message, $this->code, $previous); - } - - /** - * Get the type of the exception. - */ - public function getType(): string - { - return $this->type; - } - - /** - * Get the short version of the exception. - */ - public function getShort(): string - { - return $this->short; - } - - /** - * Check whether the error message is publishable to logging systems (e.g. Sentry). - */ - public function isPublishable(): bool - { - return $this->publish; - } -} diff --git a/src/Executor/ExecutionBadJsonException.php b/src/Executor/ExecutionBadJsonException.php new file mode 100644 index 0000000..0820369 --- /dev/null +++ b/src/Executor/ExecutionBadJsonException.php @@ -0,0 +1,11 @@ += 10) { // Enforced timeout of 10s - throw new ExecutorException(ExecutorException::RUNTIME_TIMEOUT); + throw new RuntimeTimeoutException(); } $runtime = $this->runtimes->get($runtimeName); @@ -81,7 +88,7 @@ public function getLogs(string $runtimeId, int $timeout, Response $response): vo $checkStart = \microtime(true); while (true) { if (\microtime(true) - $checkStart >= $timeout) { - throw new ExecutorException(ExecutorException::LOGS_TIMEOUT); + throw new LogsTimeoutException(); } if (\file_exists($tmpLogging . '/logs.txt') && \file_exists($tmpLogging . '/timings.txt')) { @@ -188,7 +195,7 @@ public function executeCommand(string $runtimeId, string $command, int $timeout) $runtimeName = System::getHostname() . '-' . $runtimeId; if (!$this->runtimes->exists($runtimeName)) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND); + throw new RuntimeNotFoundException(); } $commands = [ @@ -203,9 +210,9 @@ public function executeCommand(string $runtimeId, string $command, int $timeout) $this->orchestration->execute($runtimeName, $commands, $output, [], $timeout); return $output; } catch (TimeoutException $e) { - throw new ExecutorException(ExecutorException::COMMAND_TIMEOUT, previous: $e); + throw new CommandTimeoutException(previous: $e); } catch (OrchestrationException $e) { - throw new ExecutorException(ExecutorException::COMMAND_FAILED, previous: $e); + throw new CommandFailedException(previous: $e); } } @@ -236,10 +243,10 @@ public function createRuntime( if ($this->runtimes->exists($runtimeName)) { $existingRuntime = $this->runtimes->get($runtimeName); if ($existingRuntime instanceof \OpenRuntimes\Executor\Runner\Runtime && $existingRuntime->status === 'pending') { - throw new ExecutorException(ExecutorException::RUNTIME_CONFLICT, 'A runtime with the same ID is already being created. Attempt a execution soon.'); + throw new RuntimeConflictException('A runtime with the same ID is already being created. Attempt a execution soon.'); } - throw new ExecutorException(ExecutorException::RUNTIME_CONFLICT); + throw new RuntimeConflictException(); } /** @var array $container */ @@ -373,7 +380,7 @@ public function createRuntime( ); if (!$status) { - throw new ExecutorException(ExecutorException::RUNTIME_FAILED, 'Failed to create runtime: ' . $stdout); + throw new RuntimeFailedException('Failed to create runtime: ' . $stdout); } if ($version === 'v2') { @@ -386,7 +393,7 @@ public function createRuntime( $output = Logs::get($runtimeName); } } catch (Throwable $err) { - throw new ExecutorException(ExecutorException::RUNTIME_FAILED, $err->getMessage(), null, $err); + throw new RuntimeFailedException($err->getMessage(), $err); } } @@ -510,16 +517,13 @@ public function deleteRuntime(string $runtimeId): void $runtimeName = System::getHostname() . '-' . $runtimeId; if (!$this->runtimes->exists($runtimeName)) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND); + throw new RuntimeNotFoundException(); } $this->orchestration->remove($runtimeName, true); $this->runtimes->remove($runtimeName); } - /** - * @throws ExecutorException - */ public function createExecution( string $runtimeId, ?string $payload, @@ -550,7 +554,7 @@ public function createExecution( // Prepare runtime if (!$this->runtimes->exists($runtimeName)) { if ($image === '' || $image === '0' || ($source === '' || $source === '0')) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND, 'Runtime not found. Please start it first or provide runtime-related parameters.'); + throw new RuntimeNotFoundException('Runtime not found. Please start it first or provide runtime-related parameters.'); } // Prepare request to executor @@ -604,7 +608,7 @@ public function createExecution( while (true) { // If timeout is passed, stop and return error if (\microtime(true) - $prepareStart >= $timeout) { - throw new ExecutorException(ExecutorException::RUNTIME_TIMEOUT); + throw new RuntimeTimeoutException(); } ['errNo' => $errNo, 'error' => $error, 'statusCode' => $statusCode, 'executorResponse' => $executorResponse] = \call_user_func($sendCreateRuntimeRequest); @@ -647,12 +651,12 @@ public function createExecution( while (true) { // If timeout is passed, stop and return error if (\microtime(true) - $launchStart >= $timeout) { - throw new ExecutorException(ExecutorException::RUNTIME_TIMEOUT); + throw new RuntimeTimeoutException(); } $runtimeStatus = $this->runtimes->get($runtimeName); if (!$runtimeStatus instanceof \OpenRuntimes\Executor\Runner\Runtime) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND, 'Runtime no longer exists.'); + throw new RuntimeNotFoundException('Runtime no longer exists.'); } if ($runtimeStatus->status !== 'pending') { @@ -668,7 +672,7 @@ public function createExecution( // Ensure we have secret $runtime = $this->runtimes->get($runtimeName); if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND, 'Runtime secret not found. Please re-create the runtime.', 500); + throw new \Exception('Runtime secret not found. Please re-create the runtime.'); } $hostname = $runtime->hostname; @@ -906,7 +910,7 @@ public function createExecution( while (true) { // If timeout is passed, stop and return error if (\microtime(true) - $pingStart >= $timeout) { - throw new ExecutorException(ExecutorException::RUNTIME_TIMEOUT); + throw new RuntimeTimeoutException(); } $online = $validator->isValid($hostname . ':' . 3000); @@ -956,7 +960,7 @@ public function createExecution( if ($executionResponse['errNo'] !== CURLE_OK) { // Intended timeout error for v2 functions if ($version === 'v2' && $executionResponse['errNo'] === SOCKET_ETIMEDOUT) { - throw new ExecutorException(ExecutorException::EXECUTION_TIMEOUT, $executionResponse['error'], 400); + throw new ExecutionTimeoutException($executionResponse['error']); } throw new \Exception('Internal curl error has occurred within the executor! Error Number: ' . $executionResponse['errNo'], 500); @@ -1042,7 +1046,7 @@ public function getRuntime(string $name): mixed { $runtime = $this->runtimes->get($name); if (!$runtime instanceof \OpenRuntimes\Executor\Runner\Runtime) { - throw new ExecutorException(ExecutorException::RUNTIME_NOT_FOUND); + throw new RuntimeNotFoundException(); } return $runtime->toArray(); diff --git a/src/Executor/RuntimeConflictException.php b/src/Executor/RuntimeConflictException.php new file mode 100644 index 0000000..88743e6 --- /dev/null +++ b/src/Executor/RuntimeConflictException.php @@ -0,0 +1,11 @@ +