Problem/Motivation

When sending email, we got this error

TypeError: Drupal\Core\Utility\Token::replace(): Argument #2 ($data) must be of type array, null given

Steps to reproduce

Trigger this mail

$result = \Drupal::service('plugin.manager.mail')->mail(
  'system',
  'mail',
  'myemail@mydomain.com',
  'en',
  [
    'subject' => 'Test mail Drupal',
    'body' => ['Example body'],
  ],
);

Proposed resolution

diff --git a/www/core/modules/system/src/Hook/SystemHooks.php b/www/core/modules/system/src/Hook/SystemHooks.php
index aa03ea091..f6a39f003 100644
--- a/www/core/modules/system/src/Hook/SystemHooks.php
+++ b/www/core/modules/system/src/Hook/SystemHooks.php
@@ -352,9 +352,9 @@ public function cron(): void {
   #[Hook('mail')]
   public function mail($key, &$message, $params): void {
     $token_service = \Drupal::token();
-    $context = $params['context'];
-    $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'], $context));
-    $body = $token_service->replace($context['message'], $context);
+    $context = $params['context'] ?? [];
+    $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'] ?? '', $context));
+    $body = $token_service->replace($context['message'] ?? '', $context);
     $message['subject'] .= str_replace(["\r", "\n"], '', $subject);
     $message['body'][] = $body;
   }

I Will provide the merge request

Issue fork drupal-3595529

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

maxpah created an issue. See original summary.

maxpah’s picture

Issue summary: View changes

cilefen’s picture

Status: Active » Needs work
Issue tags: +Needs steps to reproduce, +Needs tests
macsim’s picture

I am able to reproduce this behavior with:
drush ev "\Drupal::service('plugin.manager.mail')->mail('system', 'mail', 'myemail@mydomain.com', 'en', ['subject' => 'Test mail Drupal', 'body' => ['Example body']]);"

The crash happens because SystemHooks::mail() reads $params['context'] unconditionally (lines 354–356) and passes it directly to Token::replace() as the $data argument, which requires an array. When $params['context'] is absent, null is passed instead, triggering the TypeError.

The system module's hook_mail implementation expects $params['context'] with subject and message keys, so the correct call to avoid the crash is:
drush ev "\Drupal::service('plugin.manager.mail')->mail('system', 'mail', 'myemail@mydomain.com', 'en', ['context' => ['subject' => 'Test mail Drupal', 'message' => 'Example body']]);"

However, according to MailManagerInterface::mail(), $params is optional and defaults to an empty array — callers are not required to pass any specific keys. SystemHooks::mail() should therefore guard against a missing or null $params['context'] rather than crashing.

Applying the patch seems to fix the problem, but the guard may be too permissive: when $params['context'] is absent, the hook silently sends an email with an empty subject and body instead of logging a warning or returning early. The caller gets no indication that something went wrong.

cilefen’s picture

macsim’s picture

I've been working on a fix for this issue and have a working patch (with kernel tests), but I'd like to get alignment on the right direction before finalizing it. The TypeError occurs because SystemHooks::mail() passes $params['context'] directly to Token::replace() without checking whether it's actually set and is an array.

There are a few ways to address this, each with different trade-offs:

Option A — Silent defensive fix (minimal patch)
Add null-coalescing operators: $context = $params['context'] ?? [], $context['subject'] ?? '', $context['message'] ?? ''. Prevents the TypeError with no noise, but silently swallows a caller bug with no visibility.
Note: this reflects the current state of the MR on this issue — tests are not yet included.

Option B — Defensive fix with warning log
Explicitly check isset($params['context']) && is_array($params['context']) and emit a \Drupal::logger('system')->warning() when the context is absent or invalid, then fall back to an empty context. Backward-compatible, but makes misuse visible in logs. This is the direction I've taken in my current patch.

Option C — Fix at the Token::replace() level
Change the type hint of the second parameter from array to array|null (with an internal $data = $data ?? []). This addresses the TypeError at its actual source rather than patching defensive code in every caller. However it's a \Drupal\Core\Utility\Token API change and would need a proper change record.

Option D — Fix the callers + clarify the API contract
Track down modules or contrib code that call \Drupal::service('plugin.manager.mail')->mail('system', ..., $params) without providing a valid $params['context'] and fix them directly. However, if $params['context'] is not formally documented as required in hook_mail(), the callers may not be at fault — this option only makes sense paired with Option E to first establish the contract they're supposed to fulfill.

Option E — Enforce the API contract explicitly
Update the hook_mail() docblock to formally document that $params['context'] must contain subject and message keys for the system module implementation, and throw an \InvalidArgumentException (or trigger a deprecation) if that contract is violated. Strictest option, least backward-compatible.

My current patch implements Option B (with kernel tests covering the missing-context case and the happy path). I lean toward B as the immediate core fix since it prevents the TypeError, logs the problem for developers, and is fully backward-compatible — but happy to adjust based on the direction we choose.

Thoughts on which direction makes most sense?

phthlaap made their first commit to this issue’s fork.

phthlaap’s picture

Status: Needs work » Needs review

I wrote tests and applied the fix code, please help to review.

smustgrave’s picture

Status: Needs review » Needs work

Thanks @phthlaap but you can see in #7 the path forward was still being discussed.