Core Symfony Mailer throws error on transport shutdown

Created on 9 February 2024, 9 months ago

I'm trying to send emails using the Symfony Mailer component in core, with SMTP transport.
I've added smtp configs to my settings.php this way:

$config['system.mail']['interface'] = [ 'default' => 'symfony_mailer' ];
$config['system.mail']['mailer_dsn'] = [
  'scheme' => 'smtp',
  'host' => 'my.smtp.host',
  'port' => 465,
  'user' => 'myuser',
  'password' => 'mypassword',
];

SMTP mails are sent out successfully. After sending, Symfony mailer tries to log the process, which results in an error thrown:

Fatal error: Uncaught TypeError: Drupal\Core\Database\StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given, called in /app/web/core/lib/Drupal/Core/Database/Connection.php on line 557 and defined in /app/web/core/lib/Drupal/Core/Database/StatementWrapperIterator.php on line 54 TypeError: Drupal\Core\Database\StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given, called in /app/web/core/lib/Drupal/Core/Database/Connection.php on line 557 in /app/web/core/lib/Drupal/Core/Database/StatementWrapperIterator.php on line 54 Call Stack: 0.6247 15039456 1. Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->__destruct() /app/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:0 0.6247 15039456 2. Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->stop() /app/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:390 0.6247 15039776 3. Drupal\Core\Logger\LoggerChannel->debug($message = 'Email transport "Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport" stopping', $context = ???) /app/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:297 0.6247 15039776 4. Drupal\Core\Logger\LoggerChannel->log($level = 'debug', $message = 'Email transport "Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport" stopping', $context = []) /app/vendor/psr/log/src/LoggerTrait.php:127 0.6247 15040432 5. Drupal\dblog\Logger\DbLog->log($level = 7, $message = 'Email transport "Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport" stopping', $context = ['channel' => 'mail', 'link' => '', 'uid' => '1', 'request_uri' => 'http://igk-d10.dev.localhost/user/650', 'referer' => 'http://igk-d10.dev.localhost/user/650', 'ip' => '172.16.112.2', 'timestamp' => 1707476301]) /app/web/core/lib/Drupal/Core/Logger/LoggerChannel.php:127 0.6248 15042624 6. Drupal\mysql\Driver\Database\mysql\Insert->execute() /app/web/core/modules/dblog/src/Logger/DbLog.php:78 0.6248 15044328 7. Drupal\Core\Database\Connection->prepareStatement($query = 'INSERT INTO {watchdog} ("uid", "type", "message", "variables", "severity", "link", "location", "referer", "hostname", "timestamp") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, :db_insert_placeholder_8, :db_insert_placeholder_9)', $options = [], $allow_row_count = ???) /app/web/core/modules/mysql/src/Driver/Database/mysql/Insert.php:42 0.6248 15044968 8. Drupal\Core\Database\StatementWrapperIterator->__construct($connection = class Drupal\mysql\Driver\Database\mysql\Connection { protected $target = 'default'; protected $key = 'default'; protected $logger = NULL; protected $transactionLayers = []; protected $driverClasses = []; protected $statementWrapperClass = 'Drupal\\Core\\Database\\StatementWrapperIterator'; protected $transactionalDDLSupport = FALSE; protected $connection = NULL; protected $connectionOptions = ['database' => 'drupal10', 'username' => 'drupal10', 'password' => 'drupal10', 'prefix' => '', 'host' => 'database', 'port' => '3306', 'namespace' => 'Drupal\\mysql\\Driver\\Database\\mysql', 'driver' => 'mysql', 'init_commands' => [...], 'autoload' =>

It seems Symfony Mailer tries to log the message 'Email transport "Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport" stopping', but the logger does not find the database connection.
Other smtp transport messages get logged successfully:

Email transport "Symfony\Component\Mailer\Transport\Smtp\SmtpTransport" starting
...
Email transport "Symfony\Component\Mailer\Transport\Smtp\SmtpTransport" started
...
// After send, "Stopping" message can't be logged
πŸ› Bug report
Status

Active

Version

11.0 πŸ”₯

Component
MailΒ  β†’

Last updated 8 days ago

No maintainer
Created by

πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @drubb
  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen
  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    At first glance this looks similar to πŸ› Transaction autocommit during shutdown relies on unreliable object destruction order (xdebug 3.3+ enabled) Needs review where things happen in __destruct() but the order of destruction is not guaranteed, so the database connection might have been closed before the transport is shut down.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK
  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

    Had a quick test: with xdebug disabled, it also happens. The error message is a bit shorter:

    Fatal error: Uncaught TypeError: Drupal\Core\Database\StatementWrapperIterator::__construct(): Argument #2 ($clientConnection) must be of type object, null given, called in /app/web/core/lib/Drupal/Core/Database/Connection.php on line 557 and defined in /app/web/core/lib/Drupal/Core/Database/StatementWrapperIterator.php:54 Stack trace: #0 /app/web/core/lib/Drupal/Core/Database/Connection.php(557): Drupal\Core\Database\StatementWrapperIterator->__construct(Object(Drupal\mysql\Driver\Database\mysql\Connection), NULL, 'INSERT INTO "wa...', Array, false) #1 /app/web/core/modules/mysql/src/Driver/Database/mysql/Insert.php(42): Drupal\Core\Database\Connection->prepareStatement('INSERT INTO "wa...', Array) #2 /app/web/core/modules/dblog/src/Logger/DbLog.php(78): Drupal\mysql\Driver\Database\mysql\Insert->execute() #3 /app/web/core/lib/Drupal/Core/Logger/LoggerChannel.php(127): Drupal\dblog\Logger\DbLog->log(7, 'Email transport...', Array) #4 /app/vendor/psr/log/src/LoggerTrait.php(127): Drupal\Core\Logger\LoggerChannel->log(7, 'Email transport...', Array) #5 /app/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php(297): Drupal\Core\Logger\LoggerChannel->debug('Email transport...') #6 /app/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php(390): Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->stop() #7 [internal function]: Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->__destruct() #8 {main} thrown in /app/web/core/lib/Drupal/Core/Database/StatementWrapperIterator.php on line 54
    
  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

    This is how the connection object looks in DbLog.php, right before the insert statement:

    Drupal\mysql\Driver\Database\mysql\Connection Object
    (
        [target:protected] => default
        [key:protected] => default
        [logger:protected] => 
        [transactionLayers:protected] => Array
            (
            )
    
        [driverClasses:protected] => Array
            (
            )
    
        [statementWrapperClass:protected] => Drupal\Core\Database\StatementWrapperIterator
        [transactionalDDLSupport:protected] => 
        [connection:protected] => 
        [connectionOptions:protected] => Array
            (
                [database] => drupal10
                [username] => drupal10
                [password] => drupal10
                [prefix] => 
                [host] => database
                [port] => 3306
                [namespace] => Drupal\mysql\Driver\Database\mysql
                [driver] => mysql
                [init_commands] => Array
                    (
                        [isolation_level] => SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
                        [sql_mode] => SET sql_mode = 'ANSI,TRADITIONAL'
                    )
    
                [autoload] => core/modules/mysql/src/Driver/Database/mysql/
                [pdo] => Array
                    (
                        [3] => 2
                        [1000] => 1
                        [1005] => 1
                        [20] => 1
                        [1013] => 
                        [17] => 1
                    )
    
            )
    
        [schema:protected] => 
        [prefix:protected] => 
        [tablePlaceholderReplacements:protected] => Array
            (
                [0] => "
                [1] => "
            )
    
        [prefixes:protected] => Array
            (
            )
    
        [prefixSearch:protected] => Array
            (
            )
    
        [prefixReplace:protected] => Array
            (
            )
    
        [unprefixedTablesMap:protected] => Array
            (
            )
    
        [escapedTables:protected] => Array
            (
                [config] => config
                [key_value] => key_value
                [key_value_expire] => key_value_expire
                [path_alias] => path_alias
                [config_pages] => config_pages
                [crop_field_data] => crop_field_data
                [menu_tree] => menu_tree
                [sessions] => sessions
                [users_field_data] => users_field_data
            )
    
        [escapedFields:protected] => Array
            (
                [] => 
                [collection] => "collection"
                [name] => "name"
                [config.name] => "config"."name"
                [uid] => "uid"
                [type] => "type"
                [message] => "message"
                [variables] => "variables"
                [severity] => "severity"
                [link] => "link"
                [location] => "location"
                [referer] => "referer"
                [hostname] => "hostname"
                [timestamp] => "timestamp"
                [base_table.status] => "base_table"."status"
                [base_table.path] => "base_table"."path"
                [base_table.langcode] => "base_table"."langcode"
                [base_table.alias] => "base_table"."alias"
                [base_table.id] => "base_table"."id"
                [config_pages.type] => "config_pages"."type"
                [config_pages.context] => "config_pages"."context"
                [cfd.uri] => "cfd"."uri"
                [cfd.type] => "cfd"."type"
                [cfd.cid] => "cfd"."cid"
                [menu_name] => "menu_name"
                [expanded] => "expanded"
                [has_children] => "has_children"
                [enabled] => "enabled"
                [parent] => "parent"
                [id] => "id"
                [menu_tree.id] => "menu_tree"."id"
                [sid] => "sid"
                [session] => "session"
                [access] => "access"
            )
    
        [escapedAliases:protected] => Array
            (
                [name] => "name"
                [config] => "config"
                [path] => "path"
                [alias] => "alias"
                [base_table] => "base_table"
                [id] => "id"
                [langcode] => "langcode"
                [base_table_id] => "base_table_id"
                [config_pages] => "config_pages"
                [type] => "type"
                [cid] => "cid"
                [cfd] => "cfd"
                [menu_tree] => "menu_tree"
                [expression] => "expression"
                [sessions] => "sessions"
            )
    
        [rootTransactionEndCallbacks:protected] => Array
            (
            )
    
        [identifierQuotes:protected] => Array
            (
                [0] => "
                [1] => "
            )
    
        [enabledEvents:Drupal\Core\Database\Connection:private] => Array
            (
            )
    
        [transactionManager:protected] => Drupal\mysql\Driver\Database\mysql\TransactionManager Object
            (
                [stack:Drupal\Core\Database\Transaction\TransactionManagerBase:private] => Array
                    (
                    )
    
                [voidedItems:Drupal\Core\Database\Transaction\TransactionManagerBase:private] => Array
                    (
                    )
    
                [postTransactionCallbacks:Drupal\Core\Database\Transaction\TransactionManagerBase:private] => Array
                    (
                    )
    
                [connection:protected] => Drupal\mysql\Driver\Database\mysql\Connection Object
     *RECURSION*
            )
    
        [needsCleanup:protected] => 
        [serverVersion:Drupal\mysql\Driver\Database\mysql\Connection:private] => 
    )
    
  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

    The stack trace in PhpStorm starts with the __destruct function of SmtpTransport.php, there the connection seems to be missing already:

    DbLog.php:61, Drupal\dblog\Logger\DbLog->log()
    LoggerChannel.php:127, Drupal\Core\Logger\LoggerChannel->log()
    LoggerTrait.php:127, Drupal\Core\Logger\LoggerChannel->debug()
    SmtpTransport.php:297, Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->stop()
    SmtpTransport.php:390, Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->__destruct()

  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    I'm seeing the same issue. Without XDebug this can be reproduced by using a drush command to send an email. The database connection is gone before the symfony mailer destructor.

    It seems that the symfony mail logging is a bit too verbose anyways. By turning that off, the issue may be gone?

  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

    This was my thought, too. However, I have no idea how to do this. Logging is part of the smtp mail transport class of Symfony Mailer, not done in Drupal. This class has a constructor where it initializes a logger, at that time with the correct database connection. One might be able to patch it, but...

    Logging the transport stop is done in the destructor of the same class, at this point the logger property is missing the database connection (null).

    The destructor calls a stop() method, where logging is done (and fails).

  • πŸ‡©πŸ‡ͺGermany drubb Sindelfingen

    Here's a workaround I'm using now to disable logging and thus prevent errors:

          $email
            ->to($message['to'])
            ->subject($message['subject'])
            ->html($message['html'])
            ->text($message['body']);
    
          // Disable logging, it's verbose and throws exceptions
          // on transport shutdown.
          $this->logger = new NullLogger();
          $this->getMailer()->send($email);
          return TRUE;
        } catch (\Exception|TransportExceptionInterface $e) {
          return FALSE;
        }
    
  • πŸ‡¨πŸ‡­Switzerland znerol

    Indeed. The log messages produced by the symfony mailer component are useless for most production setups.

    % grep -i 'log[^- ]*->' -r vendor/symfony/mailer
    vendor/symfony/mailer/DataCollector/MessageDataCollector.php:        $this->events = $logger->getEvents();
    vendor/symfony/mailer/Transport/AbstractTransport.php:            $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
    vendor/symfony/mailer/Transport/SendmailTransport.php:        $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
    vendor/symfony/mailer/Transport/SendmailTransport.php:        $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:                $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:        $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:        $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:        $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:            $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
    vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:            $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
    

    Also, symfony docs recommend to use event subscribers and the getDebug() method in order to gather useful information.

  • πŸ‡ΊπŸ‡ΈUnited States m.stenta

    I am experiencing the same issue when an email is sent via Drush. With or without XDebug.

    Notably, I am not seeing any errors in Apache logs when I test the password reset form via /user/password in the browser. So it seems that only Drush is affected?

    Here's a workaround I'm using now to disable logging and thus prevent errors:

    @drubb Where are you implementing this workaround?

    In my case, I'm trying to send an email from a custom Drush command, which is using the plugin.manager.mail service's mail() method, along with a hook_mail() implementation that builds the subject, body, etc.

    You appear to be doing something different, so I'm curious to understand your context a bit better in hopes that I can replicate the workaround in mine.

  • πŸ‡ΊπŸ‡ΈUnited States m.stenta

    Notably, I am not seeing any errors in Apache logs when I test the password reset form via /user/password in the browser. So it seems that only Drush is affected?

    Correction: I *am* seeing errors in Apache logs for /user/password (without Xdebug).

  • Status changed to Needs review 6 months ago
  • πŸ‡ΊπŸ‡ΈUnited States m.stenta

    @drubb Where are you implementing this workaround?

    Nevermind I found it. :-)

    I started a draft MR with this change. Also attaching a patch for use with cweagans/composer-patches.

  • Pipeline finished with Failed
    6 months ago
    Total: 578s
    #168656
  • πŸ‡¨πŸ‡­Switzerland znerol

    I think it would be nice to have this configurable in a similar manner as twig debugging (see ✨ Make it easier for theme builders to enable Twig debugging and disable render cache Fixed and πŸ“Œ Move twig_debug and other development toggles into raw key/value Fixed ).

    Note that with πŸ“Œ [PP-1] Add symfony mailer transports to DIC Postponed , mailer related services will be registered in the container. Since $logger is supposed to be injected into transports (via transport factories), I think that a compiler pass based approach is sensible here.

  • Status changed to Needs work 6 months ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Thanks for reporting and working on this issue

    Believe we will need test coverage for this change.

    Also believe the failures are unrelated and may just need a rebase.

  • πŸ‡¨πŸ‡­Switzerland znerol

    Implemented approach in #16. The new setting is off by default.

  • πŸ‡¨πŸ‡­Switzerland znerol

    znerol β†’ changed the visibility of the branch 3420372-core-symfony-mailer to hidden.

  • Pipeline finished with Failed
    28 days ago
    Total: 650s
    #313729
  • Pipeline finished with Success
    28 days ago
    Total: 574s
    #313737
  • πŸ‡¨πŸ‡­Switzerland znerol

    Some manual testing led me to conclude that the debug messages aren't of any use, not even in development mode. Exposing a switch in the UI is overkill. Hence, I think it is better to simply remove the logger argument from the call into Transport::getDefaultFactories().

    I found another place where we do the same (i.e., not passing the logger into a symfony component):

    See the service definition for the Symfony router_listener, especially the last argument:

      router_listener:
        class: Symfony\Component\HttpKernel\EventListener\RouterListener
        arguments: ['@router', '@request_stack', '@router.request_context', NULL]
    

    Setting this to needs work. I hope that somebody will find the time to adapt the original PR, I'll stick around for rewiews.

  • πŸ‡¨πŸ‡­Switzerland znerol

    znerol β†’ changed the visibility of the branch 3420372-add-mailer-development-setting to hidden.

  • πŸ‡¨πŸ‡­Switzerland znerol

    znerol β†’ changed the visibility of the branch 11.x to hidden.

  • πŸ‡¨πŸ‡­Switzerland znerol

    znerol β†’ changed the visibility of the branch 3420372-core-symfony-mailer to active.

  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol

    I'm tentatively removing the needs test tag here. In order to reliably cover that corner case we'd need to add an end-to-end test which successfully sends an email message over SMTP and then asserts that there were no errors after service destruction. This is neither possible with unit tests nor with kernel tests.

    Given that we are dealing with an experimental plugin here, I'd say that we can save ourselves the effort to add a test for this.

Production build 0.71.5 2024