Skip to content

htmlToText() crashes on huge whitespace-bloated email body — preg_replace_callback(): Passing null to parameter #3 ($subject) #5433

@jonalange

Description

@jonalange

Note: I'm not a deep PHP/regex expert and don't fully understand the root cause — I investigated this together with Claude Code and am filing what we found. Happy to provide more data or test a patch.

Summary

Sending a reply to a customer fails with a Send error ("Fehler senden") whenever the conversation contains an email whose stored HTML body is very large and consists almost entirely of blank/whitespace lines (a bloated Outlook/Exchange signature). The plain-text email view (emails/customer/reply_fancy_text.blade.php) runs the body through Helper::htmlToText(), the underlying Html2Text conversion's preg_replace() returns null, and that null is then passed straight into preg_replace_callback(), which on PHP 8.1+ raises a deprecation that gets escalated to a hard error and aborts the send.

Error message

Fehler senden. preg_replace_callback(): Passing null to parameter #3 ($subject) of type array|string is deprecated (View: /…/freescout/resources/views/emails/customer/reply_fancy_text.blade.php). Message-ID: FS_reply-XXXXXX-XXXXXXXXXXXXXXXX@example.com

Environment

  • FreeScout: current release (reproduced against the bundled html2text/html2text library)
  • PHP: 8.1+ (the deprecation only surfaces on 8.1+; on some hosts it is escalated to a fatal/error)
  • Hosting: shared hosting with default/tighter PCRE limits (pcre.backtrack_limit, pcre.recursion_limit, pcre.jit)

Where it happens

resources/views/emails/customer/reply_fancy_text.blade.php renders the body:

{!! \Helper::htmlToText($thread->body, true) !!}

Helper::htmlToText() (app/Misc/Helper.php) hands the body to Html2Text, and inside the library (vendor/html2text/html2text/src/Html2Text.php, converter()):

$text = preg_replace($this->search, $this->replace, $text);            // ← can return NULL on a pathological body
$text = preg_replace_callback($this->callbackSearch, [...], $text);    // ← NULL passed here → the reported error

preg_replace() returns null when the PCRE engine hits a limit (e.g. PREG_BACKTRACK_LIMIT_ERROR, PREG_RECURSION_LIMIT_ERROR, or a JIT stack limit). The library does not check for null before passing the value to preg_replace_callback() on the next line.

Root cause of the pathological body

The triggering email is a corporate Outlook/Exchange message with a heavy HTML signature (logo + social icons + a large image). FreeScout stored the body verbatim, including a huge run of empty whitespace lines inside the signature <table>:

  • Stored body size: ~2.79 MB
  • Total lines: ~34,055
  • Blank/whitespace-only lines: ~33,961 (each ~80 spaces)
  • Actual visible text: ~1.5 KB

So >99% of the body is whitespace. Converting this giant string with the Html2Text regex set is what pushes PCRE over its limit on hosts with default/low limits, producing the null and the crash. On a host with high PCRE limits the same body does not return null, but the conversion still degrades (it collapsed to an empty string in testing), so the underlying input is clearly pathological.

Anonymized reproduction snippet

A minimal version of the stored body (real one has thousands more blank lines in the signature <td>):

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<style type="text/css" style="display:none;"> P {margin-top:0;margin-bottom:0;} </style>
</head>
<body dir="ltr">
<div class="elementToProof" style="font-family: Aptos, Calibri, sans-serif; font-size: 12pt;">
Hallo zusammen, </div>
<div class="elementToProof" style="font-family: Aptos, Calibri, sans-serif;"><br></div>
<div class="elementToProof" style="font-family: Aptos, Calibri, sans-serif;">
[…short customer question, ~1.5 KB of real text…]</div>
<div class="elementToProof"><br></div>
<div class="elementToProof">Danke vorab und freundliche Grüße</div>
<div class="elementToProof">[Name removed]</div>

<table style="width: 740px; font-family: sans-serif; font-size: 10px;" border="0" cellspacing="0" cellpadding="0">
  <tbody><tr><td align="left"><br>
    <table width="740" border="0" cellspacing="0" cellpadding="0"><tbody><tr>
      <td width="150" align="center" valign="top">
        <a href="https://example.com/" target="bold">
          <img width="200" height="82" alt="Logo" src="https://helpdesk.example.com/storage/attachment/…?id=…&token=…" border="0">
        </a>
      </td>
      <td valign="top" style="font-size: 9px;">
        <span style="color: rgb(0,128,201); font-size: 14px; font-weight: bold;">[Name removed]</span><br>
<!-- ~34,000 blank whitespace-only lines like the ones below were stored here -->
                                                                                
                                                                                
                                                                                
        <span>[Title removed]</span><br>
      </td>
    </tr></tbody></table>
  </td></tr></tbody></table>
</body>
</html>

Steps to reproduce

  1. Receive an email from Outlook/Exchange with a heavy HTML signature that includes a large run of blank/whitespace lines (body ends up multi-MB).
  2. Open the conversation and reply to the customer.
  3. On a host with default/tight PCRE limits (PHP 8.1+), the send fails with the error above; the reply is not delivered.

Expected behaviour

The reply should send. htmlToText() (or the bundled Html2Text library) should not pass a null subject to preg_replace_callback(), and ideally should degrade gracefully when the PCRE engine fails on an oversized/whitespace-bloated body.

Suggested fix (for discussion)

Guard against preg_replace() returning null in the conversion path, e.g.:

  • In Html2Text::converter(), fall back to the previous value when preg_replace()/preg_replace_callback() returns null ($text = preg_replace(...) ?? $text;), and/or
  • In Helper::htmlToText(), detect a failed conversion and fall back to a strip_tags()/whitespace-collapsed version of the body.

Independently, it may be worth collapsing excessive runs of blank lines/whitespace when storing inbound HTML bodies, so multi-MB whitespace-only signatures don't get persisted verbatim.

Workaround that helped

Raising PCRE limits on the host (and disabling PCRE-JIT) stops the immediate crash on the affected server:

pcre.backtrack_limit = 10000000
pcre.recursion_limit = 10000000
pcre.jit = 0

This is only a mitigation — the unguarded null and the storage of multi-MB whitespace bodies remain.

PHP version: PHP 8.3.31
FreeScout version: 1.8.208
Database: Mysql (10.11.15-MariaDB-log)
Are you using CloudFlare: No
Are you using non-official modules: Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions