The Plugin Generated Unexpected Output During Activation: Finding the Characters
WordPress counts the characters a plugin printed while it was being activated but never shows you what they were. The count alone narrows it down, and a few lines in a must-use plugin show you the rest.
The plugin printed something while WordPress was activating it, and WordPress counted the characters but threw the text away. The notice reads The plugin generated 3 characters of unexpected output during activation. If you notice "headers already sent" messages, problems with syndication feeds or other issues, try deactivating or removing this plugin. The number is the only clue WordPress gives you, and it is a good one.
What WordPress actually checked
activate_plugin() starts an output buffer, includes the plugin's main file through plugin_sandbox_scrape(), fires activate_plugin and the plugin's own activate_{$plugin} hook, adds the plugin to active_plugins and fires activated_plugin. Only then does it look at the buffer. If ob_get_length() is above zero, it discards the contents and returns an unexpected_output error carrying them; plugins.php passes just their length to the notice through the charsout parameter.
Three things follow from that order:
- The plugin is active. The warning is not a failed activation, and a fatal error would produce a different message.
- The output is not shown anywhere. Not in the notice, not in the log.
- Other plugins can cause it. Any callback hooked to
activate_pluginoractivated_plugin— a logging or security plugin, for instance — runs inside the same buffer, and anything it echoes is counted against the plugin you just activated.
Read the count
| Characters | Most likely cause |
|---|---|
| 1–2 | A space or a second newline after a closing ?> tag at the end of a file |
| 3 | A UTF-8 byte order mark before <?php |
| 3 × number of files | A BOM in several of the plugin's files |
| Dozens to hundreds | A PHP notice, warning or deprecation printed with WP_DEBUG_DISPLAY on |
| Hundreds to thousands | An echo or HTML in the activation routine, or print_r() debugging left in |
Notices are the biggest group today. A plugin written before PHP 8.2 often triggers dynamic property deprecations or similar warnings as soon as its file is loaded, and with debug display on, each one is HTML in the buffer.
Capture the output
Because the buffer is still open when activated_plugin fires, a must-use plugin hooked late to that action can copy its contents to the log before WordPress discards them:
<?php
// wp-content/mu-plugins/log-activation-output.php
add_action( 'activated_plugin', function ( $plugin ) {
$output = ob_get_contents();
if ( '' !== $output && false !== $output ) {
error_log( sprintf(
'Activation output from %s (%d bytes): %s',
$plugin,
strlen( $output ),
bin2hex( substr( $output, 0, 64 ) ) . ' | ' . $output
) );
}
}, PHP_INT_MAX );
Deactivate and reactivate the plugin, then read wp-content/debug.log (with debug logging on). The hex prefix makes invisible characters visible: efbbbf is a BOM, 0a a newline, 20 a space. Remove the file when you are done. If the plugin is activated silently — during a bulk update, for example — these hooks do not fire and the capture stays empty.
Find the file
For whitespace and BOMs, search the plugin directly from the server:
# Files that start with a BOM
grep -rlI $'^\xEF\xBB\xBF' wp-content/plugins/plugin-slug --include='*.php'
# Files that end with ?> followed by whitespace PHP will send
find wp-content/plugins/plugin-slug -name '*.php' -exec php -r \
'$s = file_get_contents($argv[1]); if (preg_match("/\?>\s+\z/", $s) && !preg_match("/\?>(\r\n|\n|\r)\z/", $s)) echo $argv[1], "\n";' {} \;
The second check skips files that end in ?> plus a single newline: PHP swallows one newline directly after the closing tag, so only a space or a second newline is output.
Fix by cause
BOM. Re-save the file as UTF-8 without BOM, or strip it with sed -i '1s/^\xEF\xBB\xBF//' file.php. If the plugin is someone else's and you edited it, your editor added the BOM; check its encoding setting.
Whitespace after ?>. Delete the closing ?> at the end of pure-PHP files altogether. PHP does not need it, and its absence makes this mistake impossible.
Notices and deprecations. Turn display off on any site visitors can reach and log instead:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
The warning disappears because nothing is printed, while the notices still reach the log. Report them to the plugin author; they are real problems on newer PHP versions.
An echo in the activation hook. An activation routine must not print. Messages for the user belong in an admin notice shown on the next page load, typically via a transient set during activation.
Why it matters on every request
The bytes counted during activation are not an activation-only quirk. A BOM or a stray newline is sent every time WordPress includes that file, which is on every request. Output before headers produces headers already sent warnings, broken redirects after login, invalid XML in feeds and sitemaps, and corrupted JSON from the REST API — the editor's not a valid JSON response is one of the ways it shows up.
Common mistakes
- Assuming activation failed and activating the plugin again; it was already active.
- Blaming the plugin just activated when a logging plugin hooked to
activated_pluginprinted the text. - Hiding the notice with CSS or a notice-dismissal plugin while the BOM keeps breaking feeds.
- Leaving the capture mu-plugin installed on production.
Verify
- Deactivate and reactivate the plugin: no unexpected output notice.
curl -s https://example.com/feed/ | head -c 5 | xxdstarts with<?xml, not with whitespace orefbbbf.- Log in and out: the redirect works without a headers warning.
debug.logshows no new notices from the plugin's files.
Frequently asked
- Yes. WordPress adds the plugin to the active list before it checks the output buffer, so the warning is reported on an already active plugin. That is why the message suggests deactivating it if you see side effects, rather than saying activation failed.
- Almost always a UTF-8 byte order mark at the start of one of the plugin's PHP files, added by an editor that saves UTF-8 with BOM. It is invisible in most editors and is sent before the opening PHP tag. Re-save the file as UTF-8 without BOM.
- If the count is caused by a PHP notice that only appears with debugging on, the production site may be unaffected. If it is whitespace or a BOM, the same bytes are sent on every request that loads the file, and they break redirects, feeds, REST responses and file downloads. Fix those.