feat(handler): 添加了通用异常捕获器

Signed-off-by: 李东云 <dongyun.li@luxcreo.ai>
This commit is contained in:
李东云
2022-04-28 21:25:35 +08:00
parent 74f776912c
commit b02a2cc73f
4 changed files with 1568 additions and 279 deletions

View File

@@ -15,7 +15,8 @@
},
"require": {
"php": "~8.0",
"composer/composer": "*",
"ext-redis": "^5.3",
"composer/composer": ">=2.0",
"ergebnis/http-method": "^2.2",
"hyperf/config": "^2.2",
"hyperf/constants": "^2.2",
@@ -30,11 +31,13 @@
"hyperf/translation": "^2.2",
"lmc/http-constants": "^1.2",
"roave/dont": "^1.5",
"symfony/mailer": "^6.0",
"teapot/status-code": "^1.1"
},
"require-dev": {
"firebase/php-jwt": "^6.1",
"hyperf/session": "^2.2",
"hyperf/validation": "^2.2",
"phpunit/phpunit": "^9.5",
"roave/security-advisories": "dev-latest"
},

1214
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Singularity\HDK\Utils\Exceptions\Handler;
use Hyperf\Database\Exception\QueryException;
use Hyperf\Database\Model\ModelNotFoundException;
use Hyperf\Di\Annotation\Inject;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\Framework\Logger\StdoutLogger;
use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\HttpMessage\Exception\HttpException;
use Hyperf\HttpMessage\Exception\MethodNotAllowedHttpException;
use Hyperf\HttpMessage\Exception\NotFoundHttpException;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Utils\Codec\Json;
use Hyperf\Validation\ValidationException;
use Lmc\HttpConstants\Header;
use Psr\Http\Message\ResponseInterface;
use RedisException;
use Singularity\HDK\Account\Services\Auth\AuthenticationInterface;
use Singularity\HDK\Utils\Constants\CommonErrorCode;
use Singularity\HDK\Utils\Exceptions\Unauthorized;
use Singularity\HDK\Utils\Exceptions\ValidateException;
use Throwable;
// use App\Exception\Unauthorized;
// use Hyperf\HttpMessage\Cookie\Cookie;
class ClassicHandler extends ExceptionHandler
{
/**
* @Inject(required=false)
*
* @var \Hyperf\HttpServer\Contract\RequestInterface|null
*/
private ?RequestInterface $request;
/**
* @Inject
* @var \Hyperf\Framework\Logger\StdoutLogger
*/
private StdoutLogger $logger;
/**
* @Inject()
* @var \Singularity\HDK\Account\Services\Auth\AuthenticationInterface
*/
private AuthenticationInterface $authentication;
/**
* {@inheritDoc}
*/
public function handle(
Throwable $throwable,
ResponseInterface $response
): ResponseInterface {
// 阻止异常冒泡
$this->stopPropagation();
$is_testing = config('app_status') === true;
$this->request?->url();
$is_debug = $this->request?->hasHeader('Postman-Token')
|| $this->request?->header('User-Agent') === 'apifox/2.1.8 (https://www.apifox.cn)';
$error_type = get_class($throwable);
$request_time = date('Y-m-d H:i:s');
$request_data = json_encode($this->request?->getParsedBody(), JSON_UNESCAPED_UNICODE);
$request_headers = json_encode($this->request?->getHeaders(), JSON_UNESCAPED_UNICODE);
// 901 程序语法错误
// 902 SQL 语法错误
if ($throwable instanceof QueryException) {
$code = CommonErrorCode::PROGRAM_SQL_ERROR;
if ($throwable->getCode() === '42S22') {
$code = CommonErrorCode::PROGRAM_SQL_COLUMN_NOT_FOUND;
}
$data = [
'code' => $code,
'message' => CommonErrorCode::getMessage(
$is_testing
? $code
: CommonErrorCode::PROGRAM_SQL_ERROR
),
'details' => $is_testing
? [
'sql' => $throwable->getSql(),
'error' => $throwable->getMessage(),
]
: [],
];
}
// 101 请求方式错误
if ($throwable instanceof MethodNotAllowedHttpException) {
$message = explode(': ', $throwable->getMessage());
$allow_method = explode(', ', $message[1]);
$code = CommonErrorCode::REQUEST_METHOD_ERROR;
$data = [
'code' => $code,
'message' => CommonErrorCode::getMessage($code, [
'methods' => join(', ', $allow_method),
]),
'currentMethod' => $this->request?->getMethod(),
'allowedMethod' => $allow_method,
];
}
// 验证失败
if ($throwable instanceof BadRequestHttpException) {
$data = [
'code' => CommonErrorCode::REQUEST_PARAMS_ERROR,
'message' => $is_testing
? $throwable->getMessage()
: CommonErrorCode::getMessage(CommonErrorCode::SERVER_ERROR),
];
}
if ($throwable instanceof ValidationException) {
$data = $throwable->validator->errors()->first();
if (is_numeric($data)) {
$code = (int)$data;
$data = CommonErrorCode::getMessage($data);
}
$data = [
'code' => $code ?? CommonErrorCode::REQUEST_PARAMS_ERROR,
'message' => $data,
];
}
if ($throwable instanceof ValidateException) {
$code = $throwable->getCode();
$data = [
'code' => $code,
'message' => CommonErrorCode::getMessage($code, [
'param' => $throwable->getFieldName(),
]),
];
if ($is_debug) {
$data['currentValue'] = $throwable->getCurrentValue();
$data['availableValue'] = $throwable->getAvailableValue();
}
}
// 路由不存在
if ($throwable instanceof NotFoundHttpException) {
$code = CommonErrorCode::ROUTE_NOT_FOUND;
$data = [
'code' => $code,
'message' => CommonErrorCode::getMessage($code),
];
}
// 模型不存在
if ($throwable instanceof ModelNotFoundException) {
$code = empty($throwable->getCode()) ? CommonErrorCode::MODEL_NOT_FOUND : $throwable->getCode();
$message = empty($throwable->getCode()) ? CommonErrorCode::getMessage($code, [
'resource' => '资源',
]) : $throwable->getMessage();
$data = [
'code' => $code,
'message' => $message,
];
}
// 未登录或验证失败
if ($throwable instanceof Unauthorized) {
$invalidate_result = $this->authentication->invalid();
$response = $response
->withHeader(
Header::WWW_AUTHENTICATE,
join(' ', [
$throwable->getAuthenticationType(),
'realm="' . config('app_name') . '"',
'charset="UTF-8"',
])
);
switch (get_class($invalidate_result)) {
case Cookie::class:
$response = $response->withHeader(
'Set-Cookie',
(string)$invalidate_result
);
break;
default:
break;
}
}
// 300 服务出错
// 303 缓存异常
if ($throwable instanceof RedisException) {
$code = CommonErrorCode::SERVER_CACHE_REDIS_ERROR;
if ($throwable->getMessage() === 'Connection refused') {
$code = CommonErrorCode::SERVER_CACHE_REDIS_REFUSED_ERROR;
}
$data = [
'code' => $code,
'message' => CommonErrorCode::getMessage(
$is_testing
? $code
: CommonErrorCode::SERVER_CACHE_REDIS_ERROR
),
];
}
if (empty($data)) {
// 其他情况
$data = [
'code' => $is_testing
? ($throwable->getCode() == 0
? CommonErrorCode::SERVER_ERROR
: $throwable->getCode())
: CommonErrorCode::SERVER_ERROR,
'message' => $is_testing ? $throwable->getMessage() : __(
CommonErrorCode::getMessage(CommonErrorCode::SERVER_ERROR)
),
];
// 其他错误
if ($throwable instanceof HttpException) {
$data = [
'code' => $throwable->getCode() ?? $throwable->getStatusCode(),
'message' => $throwable->getMessage(),
];
}
}
$response = $response->withHeader(
Header::CONTENT_TYPE,
'application/json; charset=utf-8'
);
if ($is_debug && $is_testing) {
$data['trace'] = [
'errorType' => $error_type,
'errorTrack' => $throwable->getTrace(),
];
}
$cookies = json_encode($this->request->getCookieParams(), JSON_UNESCAPED_UNICODE);
$this->logger->error(
<<<ERROR_LOG
TYPE: $error_type
[{$data['code']}] {$data['message']}
{$throwable->getMessage()}
-------------------------------
REQUEST_TIME: $request_time
-------------------------------
REQUEST_HEADERS:
$request_headers
-------------------------------
REQUEST_COOKIES:
$cookies
-------------------------------
REQUEST_METHOD:
{$this->request?->getMethod()}
-------------------------------
REQUEST_URL:
{$this->request?->getUri()}
-------------------------------
REQUEST_QUERY:
{$this->request?->getQueryString()}
-------------------------------
REQUEST_DATA:
$request_data
-------------------------------
TRACE:
{$throwable->getTraceAsString()}
===============================
ERROR_LOG
);
$data['data'] ??= [];
$data = Json::encode($data);
return $response
->withBody(
new SwooleStream($data)
);
}
/**
* 判断该异常处理器是否要对该异常进行处理.
*/
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Singularity\HDK\Utils\Exceptions\Handler;
use Fig\Http\Message\StatusCodeInterface;
use Hyperf\Database\Exception\QueryException;
use Hyperf\Database\Model\ModelNotFoundException;
use Hyperf\Di\Annotation\Inject;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\Framework\Logger\StdoutLogger;
use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\HttpMessage\Exception\HttpException;
use Hyperf\HttpMessage\Exception\MethodNotAllowedHttpException;
use Hyperf\HttpMessage\Exception\NotFoundHttpException;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Validation\ValidationException;
use Lmc\HttpConstants\Header;
use Psr\Http\Message\ResponseInterface;
use RedisException;
use Singularity\HDK\Account\Services\Auth\AuthenticationInterface;
use Singularity\HDK\Utils\Constants\CommonErrorCode;
use Singularity\HDK\Utils\Exceptions\Unauthorized;
use Singularity\HDK\Utils\Exceptions\ValidateException;
use Symfony\Component\Mailer\Exception\TransportException;
use Throwable;
class RestfulHandler extends ExceptionHandler
{
/**
* @Inject(required=false)
*
* @var \Hyperf\HttpServer\Contract\RequestInterface|null
*/
private ?RequestInterface $request;
/**
* @Inject
* @var \Hyperf\Framework\Logger\StdoutLogger
*/
private StdoutLogger $logger;
/**
* @Inject
* @var \Singularity\HDK\Account\Services\Auth\AuthenticationInterface
*/
private AuthenticationInterface $tokenService;
/**
* {@inheritDoc}
*/
public function handle(
Throwable $throwable,
ResponseInterface $response
): ResponseInterface {
// 阻止异常冒泡
$this->stopPropagation();
$is_testing = config('app_status') === true;
$this->request?->url();
$is_debug = $this->request?->hasHeader('Postman-Token')
|| $this->request?->header('User-Agent') === 'apifox/2.1.8 (https://www.apifox.cn)';
$error_type = get_class($throwable);
$request_time = date('Y-m-d H:i:s');
$request_data = json_encode($this->request?->getParsedBody(), JSON_UNESCAPED_UNICODE);
$request_headers = json_encode($this->request?->getHeaders(), JSON_UNESCAPED_UNICODE);
// 901 程序语法错误
// 902 SQL 语法错误
if ($throwable instanceof QueryException) {
$code = CommonErrorCode::PROGRAM_SQL_ERROR;
if ($throwable->getCode() === '42S22') {
$code = CommonErrorCode::PROGRAM_SQL_COLUMN_NOT_FOUND;
}
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage(
$is_testing
? $code
: CommonErrorCode::PROGRAM_SQL_ERROR
),
];
if ($is_testing) {
$data['details'] = [
'sql' => $throwable->getSql(),
'error' => $throwable->getMessage(),
];
}
}
// 101 请求方式错误
if ($throwable instanceof MethodNotAllowedHttpException) {
$message = explode(': ', $throwable->getMessage());
$allow_method = explode(', ', $message[1]);
$code = CommonErrorCode::REQUEST_METHOD_ERROR;
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage($code, [
'methods' => join(', ', $allow_method),
]),
'currentMethod' => $this->request?->getMethod(),
'allowedMethod' => $allow_method,
];
}
// 验证失败
if ($throwable instanceof BadRequestHttpException) {
$data = [
'errorCode' => CommonErrorCode::REQUEST_PARAMS_ERROR,
'errorMsg' => $is_testing
? $throwable->getMessage()
: CommonErrorCode::getMessage(CommonErrorCode::SERVER_ERROR),
];
}
if ($throwable instanceof ValidationException) {
$data = $throwable->validator->errors()->first();
if (is_numeric($data)) {
$code = (int)$data;
$data = CommonErrorCode::getMessage($data);
}
$data = [
'errorCode' => $code ?? CommonErrorCode::REQUEST_PARAMS_ERROR,
'errorMsg' => $data,
];
}
if ($throwable instanceof ValidateException) {
$code = $throwable->getCode();
// $code = $is_testing ? $code : CommonErrorCode::REQUEST_PARAMS_ERROR;
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage($code, [
'param' => $throwable->getFieldName(),
]),
];
if ($is_debug) {
$data['currentValue'] = $throwable->getCurrentValue();
$data['availableValue'] = $throwable->getAvailableValue();
}
}
// 路由不存在
if ($throwable instanceof NotFoundHttpException) {
$code = CommonErrorCode::ROUTE_NOT_FOUND;
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage($code),
];
$status_code = 404;
}
// 模型不存在
if ($throwable instanceof ModelNotFoundException) {
$code = empty($throwable->getCode()) ? CommonErrorCode::MODEL_NOT_FOUND : $throwable->getCode();
$message = empty($throwable->getCode()) ? CommonErrorCode::getMessage($code, [
'resource' => '资源',
]) : $throwable->getMessage();
$data = [
'errorCode' => $code,
'errorMsg' => $message,
];
$status_code = 404;
}
// 未登录或验证失败
if ($throwable instanceof Unauthorized) {
$invalidate_result = $this->tokenService->invalid();
$response = $response
->withHeader(
Header::WWW_AUTHENTICATE,
join(' ', [
$throwable->getAuthenticationType(),
'realm="' . config('app_name') . '"',
'charset="UTF-8"',
])
);
switch (get_class($invalidate_result)) {
case Cookie::class:
$response = $response->withHeader(
'Set-Cookie',
(string)$invalidate_result
);
break;
default:
break;
}
}
// 300 服务出错
// 303 缓存异常
if ($throwable instanceof RedisException) {
$code = CommonErrorCode::SERVER_CACHE_REDIS_ERROR;
if ($throwable->getMessage() === 'Connection refused') {
$code = CommonErrorCode::SERVER_CACHE_REDIS_REFUSED_ERROR;
}
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage(
$is_testing
? $code
: CommonErrorCode::SERVER_CACHE_REDIS_ERROR
),
];
}
// 306 消息异常
// 30601 邮箱发件异常
if ($throwable instanceof TransportException) {
$code = CommonErrorCode::SERVER_MESSAGE_EMAIL_ERROR;
if (strpos($throwable->getMessage(), '500 Error: bad syntax')) {
$code = CommonErrorCode::SERVER_MESSAGE_EMAIL_NOT_FOUND;
}
$data = [
'errorCode' => $code,
'errorMsg' => CommonErrorCode::getMessage(
$is_testing
? $code
: CommonErrorCode::SERVER_MESSAGE_EMAIL_ERROR
),
];
}
if (empty($data)) {
// 其他情况
$data = [
'errorCode' => $is_testing
? ($throwable->getCode() == 0
? CommonErrorCode::SERVER_ERROR
: $throwable->getCode())
: CommonErrorCode::SERVER_ERROR,
'errorMsg' => $is_testing ? $throwable->getMessage() : __(
CommonErrorCode::getMessage(CommonErrorCode::SERVER_ERROR)
),
];
// 其他错误
if ($throwable instanceof HttpException) {
$data = [
'errorCode' => $throwable->getCode() ?? $throwable->getStatusCode(),
'errorMsg' => $throwable->getMessage(),
];
}
}
$response = $response->withHeader(
Header::CONTENT_TYPE,
'application/json; charset=utf-8'
);
if ($is_debug && $is_testing) {
$data['trace'] = [
'errorType' => $error_type,
'errorTrack' => $throwable->getTrace(),
];
}
$cookies = json_encode($this->request->getCookieParams(), JSON_UNESCAPED_UNICODE);
$this->logger->error(
<<<ERROR_LOG
TYPE: $error_type
[{$data['errorCode']}] {$data['errorMsg']}
{$throwable->getMessage()}
-------------------------------
REQUEST_TIME: $request_time
-------------------------------
REQUEST_HEADERS:
$request_headers
-------------------------------
REQUEST_COOKIES:
$cookies
-------------------------------
REQUEST_METHOD:
{$this->request?->getMethod()}
-------------------------------
REQUEST_URL:
{$this->request?->getUri()}
-------------------------------
REQUEST_QUERY:
{$this->request?->getQueryString()}
-------------------------------
REQUEST_DATA:
$request_data
-------------------------------
TRACE:
{$throwable->getTraceAsString()}
===============================
ERROR_LOG
);
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
// 交给下一个异常处理器
return $response
->withStatus(
$status_code ??
$throwable->status ??
$throwable->statusCode ??
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR
)
->withBody(
new SwooleStream($data)
);
// 或者不做处理直接屏蔽异常
}
/**
* 判断该异常处理器是否要对该异常进行处理.
*/
public function isValid(Throwable $throwable): bool
{
return true;
}
}