getRequest()->getPathInfo(), '/api/doc')) { return; } $exception = $event->getThrowable(); if ($exception instanceof AppException) { $err = ['code' => $exception->getErrorCode(), 'message' => $exception->getMessage()]; if ($exception->getField()) { $err['field'] = $exception->getField(); } $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [$err]], $exception->getHttpStatus() )); return; } if ($exception instanceof TooManyRequestsHttpException) { $headers = []; $retryAfter = $exception->getHeaders()['Retry-After'] ?? null; if ($retryAfter !== null) { $headers['Retry-After'] = $retryAfter; } $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_RATE_LIMIT_001', 'message' => 'درخواست‌های زیاد. لطفاً بعداً تلاش کنید']]], 429, $headers )); return; } if ($exception instanceof NotFoundHttpException) { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_NOT_FOUND_001', 'message' => 'منبع درخواستی یافت نشد']]], 404 )); return; } if ($exception instanceof AccessDeniedHttpException) { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_FORBIDDEN_001', 'message' => 'دسترسی به این منبع مجاز نیست']]], 403 )); return; } if ($exception instanceof UnauthorizedHttpException) { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]], 401 )); return; } // Security exceptions not yet wrapped into HttpException if ($exception instanceof AccessDeniedException) { $token = $this->tokenStorage->getToken(); $isAuthenticated = $token !== null && $token->getUser() !== null; if (!$isAuthenticated) { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]], 401 )); } else { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_FORBIDDEN_001', 'message' => 'دسترسی به این منبع مجاز نیست']]], 403 )); } return; } if ($exception instanceof AuthenticationException) { $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_AUTH_001', 'message' => 'احراز هویت لازم است']]], 401 )); return; } // Generic fallback: never leak stack traces or internal details in API responses. // The class/message/location go into the log MESSAGE itself so they surface in // platforms (e.g. Liara) whose default logger only prints the message string. $this->logger->error(sprintf( 'Unhandled exception: %s: %s @ %s:%d [path=%s]', $exception::class, $exception->getMessage(), $exception->getFile(), $exception->getLine(), $event->getRequest()->getPathInfo(), ), [ 'exception' => $exception, ]); $event->setResponse(new JsonResponse( ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_INTERNAL_001', 'message' => 'خطای داخلی سرور']]], 500 )); } public static function getSubscribedEvents(): array { return [KernelEvents::EXCEPTION => ['onKernelException', 10]]; } }