Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

guijs's avatar
Level 1

Mailing error exceptions is causing infinite loop on logs and jobs

In Laravel 9 / Vue SPA, I have registered a reportable on app\Exceptions\Handler.php to send a email for all exceptions. The email is dispatched on a job. The problem is that after some successful tests, this caused an infinite loop of error logging and also dispatched tens of thousands of jobs and failed jobs, in production...

This is the full code on app\Exceptions\Handler.php:

<?php

namespace App\Exceptions;

use App\Jobs\MailException;
use App\Mail\InternalNotification;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Mail;
use Throwable;
use Exception;


class Handler extends ExceptionHandler
{

    /**
     * A list of exception types with their corresponding custom log levels.
     *
     * @var array<class-string<\Throwable>, \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 for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {

        // now commented to avoid errors
        $this->reportable(function (Throwable $e) {
            $this->sendEmail($e);
        });
    }

    /**
     * Report or log an exception.
     *
     * @param  \Throwable  $exception
     * @return void
     */
    public function report(Throwable $exception)
    {

        parent::report($exception);

    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Throwable  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ModelNotFoundException) {
            return response()->json([
                'error' => 'Resource not found',
            ], 404);
        }

        return parent::render($request, $exception);
    }

    /**
     * Write code on Method
     *
     * @return response()
     */
    public function sendEmail(Throwable $exception)
    {

        // now commented to avoid errors
        if (App::environment('production')) {

            $traceString = "";

            foreach ($exception->getTrace() as $index => $row) {
                $traceString .= "#" . $index . " " . json_encode($row) . "\n\n";
            }

            $emailRequest = [
                "subject" => "Exception: " . $exception->getMessage(),
                "content" => "Exception: " . $exception->getMessage() . "\n\n" . " CODE: " . $exception->getCode() . "\n\n" . " FILE: " . $exception->getFile() . "\n\n" . " LINE: " . $exception->getLine() . "\n\n" . $traceString . "\n\n>>>ERROREXCEPTION<<<"
            ];

            // send to queue
            MailException::dispatch($emailRequest);

            // send diretly
            //Mail::to('[email protected]')
            //->send(new InternalNotification($this->request));

        }

    }

}

The MailException.php job:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use App\Mail\InternalNotification;

class MailException implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $request;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($request)
    {
        $this->request = $request;
    }

    /**
     * The number of times the job may be attempted.
     *
     * @var int
     */
    public $tries = 5;

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Mail::to('[email protected]')
            ->send(new InternalNotification($this->request));
    }
}

I believe the first error was:

Exception: The stream or file "/var/www/api.example.com/public_html/example/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: Unable to create configured logger. Using emergency logger. Context: {"exception":{}}

logging.php:

<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [

    'default' => env('LOG_CHANNEL', 'stack'),

    'deprecations' => [
        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
        'trace' => true,
    ],

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily'],
            'ignore_exceptions' => false,
        ],

        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'debug'),
            'permission' => 0664,
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'debug'),
            'permission' => 0664,
            'days' => 90,
        ],

        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Laravel Log',
            'emoji' => ':boom:',
            'level' => env('LOG_LEVEL', 'critical'),
        ],

        'papertrail' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
            'handler_with' => [
                'host' => env('PAPERTRAIL_URL'),
                'port' => env('PAPERTRAIL_PORT'),
                'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
            ],
        ],

        'stderr' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
        ],

        'syslog' => [
            'driver' => 'syslog',
            'level' => env('LOG_LEVEL', 'debug'),
        ],

        'errorlog' => [
            'driver' => 'errorlog',
            'level' => env('LOG_LEVEL', 'debug'),
        ],

        'null' => [
            'driver' => 'monolog',
            'handler' => NullHandler::class,
        ],

        'emergency' => [
            'path' => storage_path('logs/laravel.log'),
        ],

    ],

];

I then checked the laravel.log file and it had 0644 permission and owner/group was www-data, but why was it created with 0644 if the daily and single channels are set with 0664 permissions? If it was created by the emergency channel which has the same storage_path('logs/laravel.log') path, should I change the filename to e.g. "emergency.log", or can I set a 0664 permission for the emergency channel? Laravel doc states that only daily and single channels have the permission option. Also, the default channel is stack and logs are created daily. The exception were being saved to the daily log files along with the laravel.log. And for some reason, some log files are owned by www-data and others are owned by my sudo user in Apache, which is also in www-data group.

Along with this error, the Handler tried to dispatch hundreds MailException jobs, which caused a "Too many attempts" error from gmail.

And at the same time, thousands of jobs were being queued to the database and caused Serialization failure like:

Exception: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: delete from `jobs` where `id` = 20585

I then commented the "sendMail" function in Handler.php, cleared the cache, cleared the jobs and failed jobs, but errors were still being logged and log files size kept raising. It only stopped after a reboot on the EC2 instance.

So, is it possible to safelly send a email on all exceptions? I also thought about setting a CRON job to check the logs for "production.ERROR" every 30 minutes.

0 likes
2 replies
Snapey's avatar

its possible to email all exceptions, but your code needs to be bulletproof, else you generate an error that needs reporting which generates an error, which needs reporting etc etc.

One approach is to make sure your handler code is completely covered by try-catch blocks to catch all exceptions.

But a better option is to subscribe to one of the many commercial log monitoring tools

guijs's avatar
Level 1

@Snapey Actually I did try-catch at first, but that immediatelly caused the "Permission denied" log error. But I believe at the time the 0664 permissions were not set yet in logging.php config and there was no laravel.log file in storage/logs. I do remember that all files permissions in storage/logs were 0664.

The initial code was:

try {

            $traceString = "";

            foreach ($exception->getTrace() as $index => $row) {
                $traceString .= "#" . $index . " " . json_encode($row) . "\n\n";
            }

            $emailRequest = [
                "subject" => "Exception: " . $exception->getMessage(),
                "content" => "Exception: " . $exception->getMessage() . "\n\n" . " CODE: " . $exception->getCode() . "\n\n" . " FILE: " . $exception->getFile() . "\n\n" . " LINE: " . $exception->getLine() . "\n\n" . $traceString . "\n\n>>>ERROREXCEPTION<<<"
            ];

            // send to queue
            MailException::dispatch($emailRequest);

            // send diretly
            //Mail::to('[email protected]')
            //->send(new InternalNotification($this->request));

        } catch (Throwable $exception) {
            Log::error($exception);
        }

So the lack of try-catch caused the loop, which triggered the emergency logging, which created laravel.log with 644 permission and blocked other drivers from logging at it... ?

BTW I have just tried logging at Emergency channel (locally):

Log::channel('emergency')->error('some-error');

and got in laravel.log:

[2023-11-13 20:23:00] laravel.EMERGENCY: Unable to create configured logger. Using emergency logger. {"exception":"[object] (ErrorException(code: 0): Undefined array key \"driver\" at C:\Users\username\repos\apiname\vendor\laravel\framework\src\Illuminate\Log\LogManager.php:213)
[stacktrace]...
[2023-11-13 20:24:39] laravel.ERROR: some-error 

Is this related? Do I need to assign a driver to emergency channel even if it does not have a driver assigned by default?

Anyway, as you said, I will consider a commercial monitoring tool.

Please or to participate in this conversation.