, \Psr\Log\LogLevel::*> */ protected $levels = [ // ]; /** * A list of the exception types that are not reported. * * @var array> */ protected $dontReport = [ // ]; /** * A list of the inputs that are never flashed to the session on validation exceptions. * * @var array */ protected $dontFlash = [ 'current_password', 'password', 'password_confirmation', ]; /** * Register the exception handling callbacks for the application. * * @return void */ public function register() { $this->reportable(function (Throwable $e) { if (config('logging.default') == 'bugsnag') { call_user_func(config('bugsnag.before_send'), $e); } }); } /** * Report or log an exception. * * @param \Throwable $exception * @return void * * @throws \Exception */ public function report(Throwable $exception) { if ($exception instanceof MailerHttpTransportException) { $email = $this->handleMailerExceptions($exception); if (! empty($email)) { return; } } parent::report($exception); } /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request * @param \Throwable $exception * @return \Symfony\Component\HttpFoundation\Response * * @throws \Throwable */ public function render($request, Throwable $exception) { if ($request->isApi()) { return $this->handleApiExceptions($request, $exception); } if (config('app.debug') === false) { return $this->handleWebExceptions($request, $exception); } return parent::render($request, $exception); } /** * Convert an authentication exception into a response. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Auth\AuthenticationException $exception * @return \Symfony\Component\HttpFoundation\Response */ protected function unauthenticated($request, AuthenticationException $exception) { // Store the current url in the session if ($request->url() !== config('app.url')) { session(['url.intended' => $request->url()]); } return $request->expectsJson() ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->to($exception->redirectTo() ?? route('login')); } protected function handleWebExceptions($request, $exception) { if ($exception instanceof NotFoundHttpException) { // ajax 404 json feedback if ($request->ajax()) { return response()->json([ 'error' => trans('errors.header.404'), ], 404); } flash(trans('errors.body.page_not_found'))->error()->important(); // normal 404 view page feedback return redirect() ->back() ->withErrors(['msg', trans('errors.body.page_not_found')]); } if ($exception instanceof ModelNotFoundException) { // ajax 404 json feedback if ($request->ajax()) { return response()->json([ 'error' => trans('errors.header.404'), ], 404); } try { $names = explode('.', $request->route()->getName()); $names[count($names) - 1] = 'index'; $route = route(implode('.', $names)); flash(trans('errors.message.record'))->warning()->important(); return redirect($route); } catch (\Exception $e) { // normal 404 view page feedback return response()->view('errors.404', [], 404); } } if ($exception instanceof FatalThrowableError) { // ajax 500 json feedback if ($request->ajax()) { return response()->json([ 'error' => trans('errors.header.500'), ], 500); } // normal 500 view page feedback return response()->view('errors.500', [], 500); } if ($exception instanceof ThrottleRequestsException) { // ajax 500 json feedback if ($request->ajax()) { return response()->json([ 'error' => $exception->getMessage(), ], 429); } } if ($exception instanceof ViewException) { $real_exception = $this->getRealException($exception, ViewException::class); if ($real_exception instanceof UnexpectedAmountException) { if ($request->ajax()) { return response()->json([ 'error' => trans('errors.message.amount'), ], 500); } return response()->view('errors.500', [ 'message' => trans('errors.message.amount'), ], 500); } } if ($exception instanceof MailerHttpTransportException) { $email = $this->handleMailerExceptions($exception); if (! empty($email)) { $message = trans('notifications.menu.invalid_email.description', ['email' => $email]); if ($request->ajax()) { return response()->json([ 'error' => $message, ], $exception->getCode()); } return response()->view('errors.403', [ 'message' => $message, ], $exception->getCode()); } } return parent::render($request, $exception); } protected function handleApiExceptions($request, $exception): Response { $replacements = $this->prepareApiReplacements($exception); $response = config('api.error_format'); array_walk_recursive($response, function (&$value, $key) use ($replacements) { if (Str::startsWith($value, ':') && isset($replacements[$value])) { $value = $replacements[$value]; } }); $response = $this->recursivelyRemoveEmptyApiReplacements($response); return new Response($response, $this->getStatusCode($exception), $this->getHeaders($exception)); } protected function handleMailerExceptions(MailerHttpTransportException $exception): string { /** * Couldn't access the SentMessage object to get the email address * https://symfony.com/doc/current/mailer.html#debugging-emails * * https://codespeedy.com/extract-email-addresses-from-a-string-in-php * https://phpliveregex.com/p/IMG */ preg_match("/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/", $exception->getMessage(), $matches); if (empty($matches[0])) { return ''; } $email = $matches[0]; event(new InvalidEmailDetected($email, $exception->getMessage())); return $email; } /** * Prepare the replacements array by gathering the keys and values. * * @param Throwable $exception * * @return array */ protected function prepareApiReplacements(Throwable $exception): array { $code = $this->getStatusCode($exception); if (! $message = $exception->getMessage()) { $message = sprintf('%d %s', $code, Response::$statusTexts[$code]); } $replacements = [ ':message' => $message, ':status_code' => $code, ]; if ($exception instanceof ResourceException && $exception->hasErrors()) { $replacements[':errors'] = $exception->getErrors(); } if ($exception instanceof ValidationException) { $replacements[':errors'] = $exception->errors(); $replacements[':status_code'] = $exception->status; } if ($code = $exception->getCode()) { $replacements[':code'] = $code; } if (config('api.debug')) { $replacements[':debug'] = [ 'line' => $exception->getLine(), 'file' => $exception->getFile(), 'class' => get_class($exception), 'trace' => explode("\n", $exception->getTraceAsString()), ]; // Attach trace of previous exception, if exists if (! is_null($exception->getPrevious())) { $currentTrace = $replacements[':debug']['trace']; $replacements[':debug']['trace'] = [ 'previous' => explode("\n", $exception->getPrevious()->getTraceAsString()), 'current' => $currentTrace, ]; } } return $replacements; } /** * Recursively remove any empty replacement values in the response array. * * @param array $input * * @return array */ protected function recursivelyRemoveEmptyApiReplacements(array $input) { foreach ($input as &$value) { if (is_array($value)) { $value = $this->recursivelyRemoveEmptyApiReplacements($value); } } return array_filter($input, function ($value) { if (is_string($value)) { return ! Str::startsWith($value, ':'); } return true; }); } /** * Get the status code from the exception. * * @param Throwable $exception * * @return int */ protected function getStatusCode(Throwable $exception): int { $code = null; if ($exception instanceof ValidationException) { $code = $exception->status; } elseif ($exception instanceof HttpExceptionInterface) { $code = $exception->getStatusCode(); } else { // By default throw 500 $code = 500; } // Be extra defensive if ($code < 100 || $code > 599) { $code = 500; } return $code; } /** * Get the headers from the exception. */ protected function getHeaders(Throwable $exception): array { return ($exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : []; } /** * Get the real exception. */ protected function getRealException(Throwable $exception, string $current): Throwable { $previous = $exception->getPrevious() ?? $exception; while (($previous instanceof $current) && $previous->getPrevious()) { $previous = $previous->getPrevious(); } return $previous; } }