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
Comments
Comment #2
maxpahComment #4
cilefen commentedComment #5
macsim commentedI 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 toToken::replace()as the$dataargument, which requires an array. When$params['context']is absent,nullis passed instead, triggering theTypeError.The
systemmodule'shook_mailimplementation expects$params['context']withsubjectandmessagekeys, 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(),$paramsis 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.Comment #6
cilefen commentedComment #7
macsim commentedI'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 toToken::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()levelChange the type hint of the second parameter from
arraytoarray|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\TokenAPI 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 inhook_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 containsubjectandmessagekeys for thesystemmodule 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?
Comment #9
phthlaap commentedI wrote tests and applied the fix code, please help to review.
Comment #10
smustgrave commentedThanks @phthlaap but you can see in #7 the path forward was still being discussed.