Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Phenix;

use Amp\Cluster\Cluster;
use Amp\Http\Server\DefaultErrorHandler;
use Amp\Http\Server\Driver\ConnectionLimitingClientFactory;
use Amp\Http\Server\Driver\ConnectionLimitingServerSocketFactory;
use Amp\Http\Server\Driver\SocketClientFactory;
use Amp\Http\Server\ExceptionHandler as ExceptionHandlerContract;
use Amp\Http\Server\Middleware;
use Amp\Http\Server\Middleware\CompressionMiddleware;
use Amp\Http\Server\Middleware\ExceptionHandlerMiddleware;
use Amp\Http\Server\Middleware\ForwardedHeaderType;
use Amp\Http\Server\RequestHandler;
use Amp\Http\Server\Router;
Expand All @@ -33,6 +34,8 @@
use Phenix\Facades\Config;
use Phenix\Facades\Route;
use Phenix\Http\Constants\Protocol;
use Phenix\Http\ErrorHandler as AppErrorHandler;
use Phenix\Http\ExceptionHandler as AppExceptionHandler;
use Phenix\Logging\LoggerFactory;
use Phenix\Runtime\Log;
use Phenix\Scheduling\TimerRegistry;
Expand Down Expand Up @@ -60,7 +63,9 @@ class App implements AppContract, Makeable

protected bool $signalTrapping = true;

protected DefaultErrorHandler $errorHandler;
protected AppErrorHandler $errorHandler;

protected ExceptionHandlerContract $exceptionHandler;

protected Protocol $protocol = Protocol::HTTP;

Expand All @@ -75,7 +80,8 @@ public function __construct(string $path)
self::$path = $path;
self::$container = new Container();

$this->errorHandler = new DefaultErrorHandler();
$this->errorHandler = new AppErrorHandler();
$this->exceptionHandler = new AppExceptionHandler($this->errorHandler);
}

public function setup(): void
Expand Down Expand Up @@ -242,18 +248,24 @@ protected function createServer(): SocketHttpServer
}

return SocketHttpServer::createForBehindProxy(
$this->logger,
ForwardedHeaderType::XForwardedFor,
$trustedProxies
logger: $this->logger,
headerType: ForwardedHeaderType::XForwardedFor,
trustedProxies: $trustedProxies,
exceptionHandler: $this->exceptionHandler
);
}

return SocketHttpServer::createForDirectAccess($this->logger);
return SocketHttpServer::createForDirectAccess(
logger: $this->logger,
exceptionHandler: $this->exceptionHandler
);
}

protected function createClusterServer(): SocketHttpServer
{
$middleware = [];
$middleware = [
new ExceptionHandlerMiddleware($this->exceptionHandler),
];
$allowedMethods = Middleware\AllowedMethodsMiddleware::DEFAULT_ALLOWED_METHODS;

if (extension_loaded('zlib')) {
Expand All @@ -271,34 +283,34 @@ protected function createClusterServer(): SocketHttpServer
$middleware[] = new Middleware\ForwardedMiddleware(ForwardedHeaderType::XForwardedFor, $trustedProxies);

return new SocketHttpServer(
$this->logger,
Cluster::getServerSocketFactory(),
new SocketClientFactory($this->logger),
$middleware,
$allowedMethods,
logger: $this->logger,
serverSocketFactory: Cluster::getServerSocketFactory(),
clientFactory: new SocketClientFactory(logger: $this->logger),
middleware: $middleware,
allowedMethods: $allowedMethods,
);
}

$connectionLimit = 1000;
$connectionLimitPerIp = 10;

$serverSocketFactory = new ConnectionLimitingServerSocketFactory(
new LocalSemaphore($connectionLimit),
Cluster::getServerSocketFactory(),
semaphore: new LocalSemaphore($connectionLimit),
socketServerFactory: Cluster::getServerSocketFactory(),
);

$clientFactory = new ConnectionLimitingClientFactory(
new SocketClientFactory($this->logger),
$this->logger,
$connectionLimitPerIp,
clientFactory: new SocketClientFactory(logger: $this->logger),
logger: $this->logger,
connectionsPerIpLimit: $connectionLimitPerIp,
);

return new SocketHttpServer(
$this->logger,
$serverSocketFactory,
$clientFactory,
$middleware,
$allowedMethods,
logger: $this->logger,
serverSocketFactory: $serverSocketFactory,
clientFactory: $clientFactory,
middleware: $middleware,
allowedMethods: $allowedMethods,
);
}

Expand Down
30 changes: 29 additions & 1 deletion src/AppProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
namespace Phenix;

use Phenix\Contracts\App as AppContract;
use Phenix\Facades\Config;
use Phenix\Runtime\ErrorHandling\GlobalErrorHandler;
use Throwable;

class AppProxy implements AppContract
{
Expand All @@ -18,16 +21,28 @@ public function __construct(

public function run(): void
{
$this->configureErrorReporting();

GlobalErrorHandler::register();

if ($this->testingMode) {
$this->app->disableSignalTrapping();
}

$this->app->run();
try {
$this->app->run();
} catch (Throwable $exception) {
GlobalErrorHandler::restore();

throw $exception;
}
}

public function stop(): void
{
$this->app->stop();

GlobalErrorHandler::restore();
}

public function swap(string $key, object $concrete): void
Expand All @@ -46,4 +61,17 @@ public function enableTestingMode(): self

return $this;
}

private function configureErrorReporting(): void
{
$debug = Config::get('app.debug') === true;

error_reporting(E_ALL);
ini_set('display_errors', $debug ? '1' : '0');
ini_set('display_startup_errors', $debug ? '1' : '0');

if ($debug) {
ini_set('log_errors', '1');
}
}
}
55 changes: 55 additions & 0 deletions src/Http/ErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Phenix\Http;

use Amp\Http\HttpStatus;
use Amp\Http\Server\ErrorHandler as ErrorHandlerContract;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Phenix\Facades\Config;

class ErrorHandler implements ErrorHandlerContract
{
public function handleError(int $status, ?string $reason = null, ?Request $request = null): Response
{
$message = $reason ?? HttpStatus::getReason($status);
$payload = [
'success' => false,
'error' => $message,
'status' => $status,
];

if ($this->shouldExposeDebugDetails()) {
$payload['debug'] = [
'reason' => $message,
'path' => $request?->getUri()->getPath(),
];
}

return $this->json($payload, $status, $reason);
}

/**
* @param array<string, mixed> $payload
*/
public function json(array $payload, int $status, ?string $reason = null): Response
{
$response = new Response(
headers: [
'content-type' => 'application/json',
],
body: json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);

$response->setStatus($status, $reason);

return $response;
}

public function shouldExposeDebugDetails(): bool
{
return Config::get('app.debug') === true;
}
}
44 changes: 44 additions & 0 deletions src/Http/ExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Phenix\Http;

use Amp\Http\HttpStatus;
use Amp\Http\Server\ExceptionHandler as ExceptionHandlerContract;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Throwable;

class ExceptionHandler implements ExceptionHandlerContract
{
public function __construct(
private readonly ErrorHandler $errorHandler
) {
}

public function handleException(Request $request, Throwable $exception): Response
{
report($exception, [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
'client' => $request->getClient()->getRemoteAddress()->toString(),
]);

if (! $this->errorHandler->shouldExposeDebugDetails()) {
return $this->errorHandler->handleError(status: HttpStatus::INTERNAL_SERVER_ERROR, request: $request);
}

return $this->errorHandler->json(payload: [
'success' => false,
'error' => $exception->getMessage(),
'status' => HttpStatus::INTERNAL_SERVER_ERROR,
'debug' => [
'exception' => $exception::class,
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'path' => $request->getUri()->getPath(),
],
], status: HttpStatus::INTERNAL_SERVER_ERROR);
}
}
115 changes: 115 additions & 0 deletions src/Runtime/ErrorHandling/GlobalErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

declare(strict_types=1);

namespace Phenix\Runtime\ErrorHandling;

use ErrorException;
use Throwable;

use function in_array;

class GlobalErrorHandler
{
private const FATAL_ERRORS = [
E_ERROR,
E_PARSE,
E_CORE_ERROR,
E_COMPILE_ERROR,
];

private static bool $registered = false;

private static bool $active = false;

/**
* @var callable|null
*/
private static $previousExceptionHandler = null;

public static function register(): void
{
if (self::$active) {
return;
}

self::$active = true;
set_error_handler(self::handleError(...));
self::$previousExceptionHandler = set_exception_handler(self::handleException(...));

if (! self::$registered) {
register_shutdown_function(self::handleShutdown(...));
self::$registered = true;
}
}

public static function restore(): void
{
if (! self::$active) {
return;
}

restore_error_handler();
restore_exception_handler();

self::$previousExceptionHandler = null;
self::$active = false;
}

public static function handleError(int $severity, string $message, string $file, int $line): bool
{
if ((error_reporting() & $severity) === 0) {
return false;
}

$exception = new ErrorException($message, 0, $severity, $file, $line);

report($exception, [
'severity' => $severity,
'source' => 'php-error',
]);

throw $exception;
}

public static function handleException(Throwable $exception): void
{
report($exception, [
'source' => 'uncaught-exception',
]);

if (self::$previousExceptionHandler !== null) {
(self::$previousExceptionHandler)($exception);
}
}

public static function handleShutdown(): void
{
self::handleShutdownError(error_get_last());
}

/**
* @param array{type: int, message: string, file: string, line: int}|null $error
*/
public static function handleShutdownError(array|null $error): void
{
if (! self::$active) {
return;
}

if ($error === null || ! in_array($error['type'], self::FATAL_ERRORS, true)) {
return;
}

report(new ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
), [
'severity' => $error['type'],
'source' => 'fatal-error',
]);
}
}
Loading
Loading