Problem/Motivation
I was trying to troubleshoot a 500 error and was stumped by this; apparently the issue is with passing translatable markup with a null placeholder to an error handler that chokes on null.
Full disclosure: most of the following diagnosis is from Claude...
-------
When DataExport::doProcessBatch() fails to create the temporary output file (for example, when a display's export_filesystem option is empty or points to an unavailable scheme), the catch block that is meant to log the failure crashes instead, and the original exception is discarded. The user sees a generic HTTP 500 with a confusing TypeError from Drupal's logger internals, and the log contains nothing about the actual cause.
Response text from the batch step (/batch?id=…&op=do):
TypeError: Drupal\Component\Utility\Html::escape(): Argument #1 ($text) must be
of type string, null given, called in …/FormattableMarkup.php on line 238 …
Drupal\Component\Render\FormattableMarkup::placeholderEscape()
Drupal\Core\StringTranslation\TranslatableMarkup->render()
Drupal\Core\StringTranslation\TranslatableMarkup->__toString()
strpos()
Drupal\Core\Logger\LogMessageParser->parseMessagePlaceholders()Steps to reproduce
- Configure a data_export display whose export_filesystem option is empty/invalid (e.g. a view whose config predates the store_in_public_file_directory → export_filesystem migration, installed on a site where that post_update had already run).
- Trigger a batched CSV export.
- getTempFile() throws InvalidStreamWrapperException; the catch block at src/Plugin/views/display/DataExport.php:830 runs and 500s.
In doProcessBatch() (src/Plugin/views/display/DataExport.php, ~lines 830–836):
catch (FileException | FileExistsException | InvalidStreamWrapperException | EntityStorageException) {
// Failed to create the file, abort the batch.
unset($context['sandbox']);
$context['success'] = FALSE;
$message = t('Could not write to temporary output file for result export (@file). Check permissions.', ['@file' => $context['sandbox']['vde_file']]);
\Drupal::logger('views_data_export')->error($message);
}- Read-after-unset():
$context['sandbox']is unset on the line above, so$context['sandbox']['vde_file']evaluates to null, making the@fileplaceholder null. - TranslatableMarkup with the null placeholder is passed as the logger message. The logger's LogMessageParser::parseMessagePlaceholders() runs strpos() on the message, which stringifies the TranslatableMarkup; rendering it calls
Html::escape(null), which throws a TypeError on PHP 8.1+ before the log can execute - The new exception means the caught exception is discarded, so your intended error message (e.g. "The filesystem scheme is not valid or available" from getTempFile()) is never logged.
- Note that the same t()-as-logger-message pattern exists at ~lines 933–934 in the "write failed" branch; it does not currently crash only because @file is non-null there.
Proposed resolution
Log a plain-string message with placeholders in the context array, and don't read sandbox after unsetting it:
catch (FileException | FileExistsException | InvalidStreamWrapperException | EntityStorageException $e) {
// Failed to create the file, abort the batch.
$context['success'] = FALSE;
\Drupal::logger('views_data_export')->error('Could not create the temporary output file for result export. Check the display\'s export filesystem configuration and permissions. Error: @message', [
'@message' => $e->getMessage(),
]);
unset($context['sandbox']);
}And at the write-failure branch (~933–934), split the logger message (plain string + context) from the exception message:
$args = ['@file' => $context['sandbox']['vde_file']];
\Drupal::logger('views_data_export')->error('Could not write to temporary output file for result export (@file). Check permissions.', $args);
throw new ServiceUnavailableHttpException(NULL, new FormattableMarkup('Could not write to temporary output file for result export (@file). Check permissions.', $args));
Comments
Comment #2
itmaybejj commented