Phase 6 (SSE 流式处理): - CancellationToken: 客户端断开检测 - SseConcurrencyGuard: 全局+Key 并发槽位管理 - UpstreamStreamClient: 上游 Provider 流式代理 - SseStreamController: SSE 生命周期编排 - RouteMiddleware: 集成 SSE 流式分支 Phase 7 (错误处理+审计): - ErrorMapper: WP_Error → OpenAI 错误格式映射 (14种) - ErrorMiddleware: 异常捕获 + OpenAI 兼容错误响应 - LogMiddleware: 审计日志写入 + 用量统计 upsert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
2.6 KiB
PHP
102 lines
2.6 KiB
PHP
<?php
|
|
/**
|
|
* Error Middleware
|
|
*
|
|
* Pipeline stage that converts errors and exceptions into
|
|
* OpenAI-compatible JSON error responses.
|
|
*
|
|
* @package WPMind\Modules\ApiGateway\Pipeline
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WPMind\Modules\ApiGateway\Pipeline;
|
|
|
|
use WPMind\Modules\ApiGateway\Error\ErrorMapper;
|
|
|
|
/**
|
|
* Class ErrorMiddleware
|
|
*
|
|
* Always executes (finally semantics). Converts WP_Error or
|
|
* uncaught exceptions into OpenAI-formatted REST responses.
|
|
*/
|
|
final class ErrorMiddleware implements GatewayStageInterface {
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public function process( GatewayRequestContext $context ): void {
|
|
$this->handle_exception( $context );
|
|
$this->handle_error( $context );
|
|
}
|
|
|
|
/**
|
|
* Convert an uncaught exception to a WP_Error if no error is set yet.
|
|
*
|
|
* Never exposes exception details to the client. The actual
|
|
* exception message is logged server-side for debugging.
|
|
*
|
|
* @param GatewayRequestContext $context Request context.
|
|
*/
|
|
private function handle_exception( GatewayRequestContext $context ): void {
|
|
if ( ! $context->has_exception() ) {
|
|
return;
|
|
}
|
|
|
|
// Log the real exception for server-side debugging.
|
|
$exception = $context->exception();
|
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
error_log(
|
|
sprintf(
|
|
'[WPMind API Gateway] Uncaught exception in request %s: %s in %s:%d',
|
|
$context->request_id(),
|
|
$exception->getMessage(),
|
|
$exception->getFile(),
|
|
$exception->getLine()
|
|
)
|
|
);
|
|
|
|
// Only set a generic error if no specific error was already set.
|
|
if ( ! $context->has_error() ) {
|
|
$context->set_error(
|
|
new \WP_Error(
|
|
'internal_error',
|
|
'An internal error occurred.',
|
|
[ 'status' => 500 ]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a WP_Error to an OpenAI-formatted REST response.
|
|
*
|
|
* @param GatewayRequestContext $context Request context.
|
|
*/
|
|
private function handle_error( GatewayRequestContext $context ): void {
|
|
if ( ! $context->has_error() ) {
|
|
return;
|
|
}
|
|
|
|
$error = $context->error();
|
|
$error_code = $error->get_error_code();
|
|
$mapping = ErrorMapper::map( $error_code );
|
|
|
|
$body = ErrorMapper::format_openai_error(
|
|
$error->get_error_message(),
|
|
$mapping['type'],
|
|
$mapping['code']
|
|
);
|
|
|
|
$response = new \WP_REST_Response( $body, $mapping['status'] );
|
|
$response->header( 'Content-Type', 'application/json' );
|
|
|
|
// Add Retry-After header for rate-limited responses.
|
|
if ( $context->retry_after_sec() > 0 ) {
|
|
$response->header( 'Retry-After', (string) $context->retry_after_sec() );
|
|
}
|
|
|
|
$context->set_rest_response( $response );
|
|
}
|
|
}
|