Opened 11 months ago
Last modified 28 hours ago
#64155 reviewing enhancement
Add stack trace to failed plugin update error notifications
| Reported by: | tlloancy | Owned by: | westonruter |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Upgrade/Install | Version: | 6.6 |
| Severity: | normal | Keywords: | has-patch has-test-info |
| Cc: | Focuses: | administration, php-compatibility |
Description
Problem
When a plugin update fails, WordPress sends an email notification (example below) indicating the failure, but no detailed error information is provided. Additionally, the debug log (wp-content/debug.log) remains empty, making it difficult to diagnose the cause of the failure.
Example Email Notification:
Bonjour ! Certaines mises à jour d’extensions ont échoué sur votre site situé à l’adresse https://yspania.com. [...] Les extensions suivantes n’ont pas pu être mises à jour. [...] - WooPayments (de la version 10.1.0 vers 10.1.1) : https://wordpress.org/plugins/woocommerce-payments/
Proposed Enhancement
Add a stack trace or detailed error information (e.g., PHP error type, file, and line number) to the email notifications and debug log for failed plugin updates. This would help site administrators identify and troubleshoot issues more effectively.
Impact
Without detailed error information, diagnosing plugin update failures is challenging, especially for non-technical users. This can lead to prolonged downtime, security risks, or reliance on external support.
Suggested Implementation
Modify the plugin update process (likely in wp-admin/includes/update.php or related files) to capture and include PHP error details and a stack trace in:
- The email notification sent to the site administrator.
- The WordPress debug log when
WP_DEBUG_LOGis enabled.
Consider using PHP's debug_backtrace() or similar functions to generate the stack trace and include it only when debugging is enabled to avoid exposing sensitive information in production environments.
Steps to Reproduce
- Enable
WP_DEBUGandWP_DEBUG_LOGinwp-config.php. - Trigger a plugin update that fails (e.g., due to a timeout, permissions issue, or fatal error).
- Check the email notification and
wp-content/debug.logfor error details. - Observe that no stack trace or detailed error information is provided.
Additional Notes
This enhancement would align with WordPress's goal of improving developer and administrator experience by providing better tools for debugging. It could also reduce the number of support requests on forums like https://wpfr.net/support.
Change History (44)
#2
@
11 months ago
Can I assume that after the update failure the plugin still shows an update available?
If this is the case, what happens during a manual plugin update?
#3
@
11 months ago
@afragen: The auto-update for WooPayments (10.1.0 → 10.1.1) on https://yspania.com failed at 09:49:57 UTC (email: "Certaines mises à jour d’extensions ont échoué"), with no error details or stack trace in the email and an empty debug.log (WP_DEBUG_LOG enabled). Server logs confirm no downtime, DDoS, or OOM killer issues (wp-cron ran fine). The second attempt at 20:49:02 UTC succeeded (email: "Certaines extensions ont été mises à jour"). This random failure-then-success, with no debug info, mirrors my SlideCrafter Reborn case and highlights the need for stack traces in emails/logs to diagnose unpredictable issues. Can explore test scenarios if helpful.
#4
@
11 months ago
@tlloancy my question isn't that it fails. It's whether the rollback is working and the site still shows an update available.
#5
@
11 months ago
@afragen: Yes — after the failed auto-update at 09:49:57 UTC:
- "Mettre à jour maintenant" link was present in the update row.
- Update still showed as scheduled ("planifiée dans X heures").
- I did NOT click manual update — let the next auto-run do it.
- It succeeded at 20:49:02 UTC.
#6
@
11 months ago
@tlloancy I consider this a success and exactly what’s supposed to happen.
I will tell you that if you have WP_DEBUG set to true you will see more in your debug.log.
I have seen seemingly silent PHP Fatals in the log, probably caused by some script trying to access pages and functions directly. If these occur during the auto-update process the PHP Fatal can stop any running process and the update will fail.
All this to say, if auto-update is left alone it is likely to succeed either on the first or second attempt.
#7
@
11 months ago
just now
@afragen Here is hard proof from another real production site — two consecutive auto-update failures of the same plugin, both with:
phpdefine( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
Result:
wp-content/debug.log → completely empty
No PHP error, no timeout, no memory issue
Manual update works instantly
Plugin in question: Newsletter
→ Version 9.0.5 → 9.0.6
Failure #1 – 2025-10-21 15:08:13
textSubject: [GATASYA YOGA] Certaines mises à jour d’extensions ont échoué
Recipient: contact@…
Email body (via WP Mail Logging iframe preview):
"Les extensions suivantes n’ont pas pu être mises à jour : Newsletter"
No error code. No reason. No stack trace.
Failure #2 – 2025-10-22 03:19:37
textSubject: [GATASYA YOGA] Certaines mises à jour d’extensions ont échoué
Recipient: contact@…
Same email. Same plugin: Newsletter
Same silence in logs
Retry later → succeeded
Manual update from admin → instant success
Source: WP Mail Logging Plugin
<!-- Email #1 --> <div class="wp-mail-logging-modal-row-value">2025-10-21 15:08:13</div> <div class="wp-mail-logging-modal-row-value">[GATASYA YOGA] Certaines mises à jour d’extensions ont échoué</div> <!-- Email #2 --> <div class="wp-mail-logging-modal-row-value">2025-10-22 3:19:37</div> <div class="wp-mail-logging-modal-row-value">[GATASYA YOGA] Certaines mises à jour d’extensions ont échoué</div>
No WP_Error code. No message. No file/line. No stack trace.
This is not a fatal PHP error
→ Fatals are logged with WP_DEBUG_LOG
→ These are not
This is not a download/network failure
→ download_failed → logged
→ Here: total silence
And then the manual update works, Which of course does'nt help at all the comprehension
Newsletter Désactiver | Traduire L’extension Newsletter permet de créer votre propre liste d’abonnés, d’envoyer des newsletters en masse, et de construire votre réseau professionnel. Avant de mettre à jour, visitez cette page, pour connaître les derniers changements. Version 9.0.6 | Par Stefano Lissa & The Newsletter Team | Afficher les détails Désactiver les mises à jour auto Mis à jour !
Proposed Fix (debug-only, zero production impact) Something along this line:
// File: wp-admin/includes/class-wp-automatic-updater.php
// After: $result = $upgrader->upgrade( $plugin );
if ( is_wp_error( $result ) ) {
$plugin = $this->skin->plugin;
$error_msg = $result->get_error_message();
$error_code = $result->get_error_code();
// 1. Log to debug.log (only if enabled)
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
$trace = ( defined( 'WP_DEBUG' ) && WP_DEBUG )
? "\nStack trace:\n" . ( new Exception() )->getTraceAsString()
: '';
error_log( sprintf(
"[Auto-Update Failure] Plugin: %s | Code: %s | Message: %s%s",
$plugin,
$error_code,
$error_msg,
$trace
) );
}
// 2. Enhanced email (admin-only)
$debug_info = ( defined( 'WP_DEBUG' ) && WP_DEBUG )
? "\n\nDebug: {$error_msg} (Code: {$error_code})"
: '';
$this->send_email( 'failure', $plugin, $debug_info );
}
Conclusion:
Rollback + retry = does not fix it
Manual update = required to resolve
Debugging = impossible
#8
@
11 months ago
@afragen
Additional evidence uploaded — 3 screenshots attached to this ticket to fully support the silent failure claim:
---
### Attached Screenshots:
- screenshot-1-email-failure-1.png
→ WP Mail Logging: Email #1 (2025-10-21 15:08:13)
→ Subject:
Certaines mises à jour d’extensions ont échoué→ Body clearly lists: "Newsletter"
- screenshot-2-email-failure-2.png → WP Mail Logging: Email #2 (2025-10-22 03:19:37) → Same subject, same plugin: Newsletter → No error details, no stack trace
- screenshot-3-plugin-updated-manually.png
→ WordPress admin → Plugins page
→ Newsletter now at Version 9.0.6
→ Status:
Mis à jour !→ Only after manual update — auto-updates failed twice
---
All with:
`php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
#9
@
11 months ago
@tlloancy why don't you put your debugging code in one of your site displaying the problem and get some data? Perhaps then we can get to the root of the problem.
#10
@
11 months ago
@tlloancy if you want I wrote a single file plugin that will fill your debug.log with all sorts of data. I have to warn you that there will be mountains of stuff to sort through.
https://gist.github.com/afragen/a09b52047bf1d7f11b622941678824cc
#11
@
11 months ago
@afragen: Thanks for the plugin — I’ll test it on a staging copy of https://yspania.com or https://gatasya-yoga.com to capture the next failure without risking production.
But this is exactly my point:
- Admins shouldn’t need a custom debug plugin to understand *why* an auto-update failed.
WP_DEBUG_LOGis on → debug.log stays empty.- Failure email → zero details.
- Manual update works, auto-retry works later → but no trace of the root cause.
Your tool is great for developers, but core should log stack traces by default in failure emails/logs — so any admin can diagnose issues like:
- Silent fatal in a plugin (
wp_cache_flush()killing cron) - Timeout / memory
- Filesystem lock
And maybe even they can help themselves after that with plugins such as WP Control for instance.
I’ll run your plugin on staging and share results if a failure occurs.
But this ticket is about making WordPress *self-diagnosing* for all users, not just those who can install debug tools.
#12
@
11 months ago
Unfortunately I don't think most users will understand a stacktrace. They really just want to know did it work or not.
#13
@
11 months ago
@afragen
Thanks for the debug plugin — it worked perfectly on the production site (no staging needed this time). Here's hard evidence from debug.log during the exact failed auto-update of *Payment Plugins for Stripe WooCommerce* (3.3.94 → 3.3.95) on 2025-10-30 at 08:48 UTC (email sent at 09:48 local time).
---
### Key Log Excerpt (filtered to 08:48)
log [30-Oct-2025 08:48:30 UTC] Plugin 'woo-stripe-payment' has been upgraded. [30-Oct-2025 08:48:32 UTC] Scraping home page... [30-Oct-2025 08:48:32 UTC] PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 32768 bytes) in /var/lib/wordpress/wp-content/plugins/advanced-custom-fields/includes/fields/class-acf-field-wysiwyg.php on line 375 [30-Oct-2025 08:48:33 UTC] wp_error_added 'plugin_update_fatal_error_rollback_successful' [30-Oct-2025 08:48:33 UTC] La mise à jour de « woo-stripe-payment » contenait une erreur fatale. La version précédente a été restaurée.
---
### What Happened
- Update succeeded → files replaced.
- Post-update check: WordPress scraped the homepage (
Scraping home page...). - ACF WYSIWYG fields loaded → memory exhausted at 128 MB.
- PHP fatal error → process killed.
- Rollback triggered → version restored.
- Email sent: *"Certaines mises à jour d’extensions ont échoué"* → zero details.
- debug.log (without your plugin): completely empty (even with
WP_DEBUG_LOG = true).
---
### Why This Proves the Ticket
- Silent failure: No
WP_Errorcode, no message, no stack trace in email or default log. - Fatal error not captured: Occurs in a cron HTTP child process → bypasses
debug.log. - Manual update works: No scraping → no memory issue.
- Rollback works, but admin is blind → cannot fix root cause (low
memory_limit+ heavy ACF frontend).
---
### Proposed Fix (improved version)
// wp-admin/includes/class-wp-automatic-updater.php
// After: $result = $upgrader->upgrade( $plugin );
if ( is_wp_error( $result ) || doing_action( 'wp_maybe_auto_update' ) ) {
$last_error = error_get_last();
$fatal_info = $last_error && $last_error['type'] === E_ERROR
? "\nFatal Error: {$last_error['message']} in {$last_error['file']}:{$last_error['line']}"
: '';
// 1. Log (only if enabled)
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
$trace = ( defined( 'WP_DEBUG' ) && WP_DEBUG )
? "\nStack trace:\n" . ( new Exception() )->getTraceAsString()
: '';
error_log( "[Auto-Update Failure] Plugin: {$this->skin->plugin} | {$result->get_error_message()}{$fatal_info}{$trace}" );
}
// 2. Enhanced email
$debug_info = ( defined( 'WP_DEBUG' ) && WP_DEBUG )
? "\n\n--- Debug ---\nError: {$result->get_error_message()}{$fatal_info}"
: '';
$this->send_email( 'failure', $this->skin->plugin, $debug_info );
}
- Uses
error_get_last()→ captures real fatal.- Only in debug mode → no production risk.
- Works for all silent failures (memory, timeout, etc.).
---
### Attachments
- Full debug.log → [debug.log.txt](https://filebin.net/hzdhnzxqlbnu7xmm)
- Email screenshot → [email-failure-screenshot.png](https://ibb.co/Cp2WVvx4)
- Debug.log relevant part → [wp-config-debug.png](https://ibb.co/FqLBYVvR)
---
This is not just a retry issue.
This is a UX + debugging failure in core.
Admins must know *why* an update failed — especially when rollback hides the crash.
Ready to open a PR if needed.
Let me know how to help move this forward.
---
Keywords: has-patch needs-testing
Focuses: administration, php-compatibility
#14
@
11 months ago
@tlloancy this is consistent with what I said about silent PHP fatals occurring during the auto-update. The assumption is that the shouldn't always be present and eventually the auto-update succeeds assuming the update doesn't contain a PHP fatal.
That said. Why don't you try testing your patch and let us know the results. If it works to your satisfaction then make a PR. I'll be happy to review.
#15
@
11 months ago
@afragen Thank you for the feedback.
I’ve tested the patch extensively on a production-like environment with real memory exhaustion failures during auto-updates (WooCommerce, ACF, etc.).
### Results:
- Fatal error is captured in
has_fatal_error()viaerror_get_last()ordebug.log. - Stored in a global transient (
wp_last_fatal_error) before rollback. - Injected into the failure email *after*
apply_filters('auto_plugin_theme_update_email'). - No performance impact — only runs on failure.
- No file I/O — uses WordPress transients (clean, safe, atomic).
- Works with multiple plugins failing — only last fatal is shown (expected behavior).
- English message, no textdomain needed (technical debug output).
### Why this matters:
"Admins must know *why* an update failed — especially when rollback hides the crash."
Without this, the admin sees:
_"The update failed. Previous version restored."_
With this patch:
_"The update failed. Previous version restored.
LAST FATAL PHP ERROR
- [31-Oct-2025 22:41:17 UTC] PHP Fatal error: Allowed memory size of 134217728 bytes exhausted... in /woocommerce/includes/class-wc-autoloader.php on line 58"_
---
### Patch attached:
class-wp-automatic-updater.php— 2 small, safe changes
# diff class-wp-automatic-updater.php /usr/share/wordpress/wp-admin/includes/class-wp-automatic-updater.php
1542a1543,1555
> // [PATCH] RÉCUPÉRER L'ERREUR DEPUIS LE TRANSIENT
> if ( defined( 'WP_DEBUG' ) && WP_DEBUG && $type === 'fail' ) {
> $transient_key = 'wp_last_fatal_error';
> $fatal_error = get_transient( $transient_key );
> if ( $fatal_error ) {
> $email['body'] .= "\n\n=== LAST FATAL ERROR (PHP) ===\n";
> $email['body'] .= "• " . $fatal_error . "\n";
> $email['body'] .= "========================================\n";
> delete_transient( $transient_key );
> }
> }
> // [FIN PATCH]
>
1829a1843,1863
>
> if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
> $fatal_error = null;
>
> $last_error = error_get_last();
> if ( $last_error && in_array( $last_error['type'], [1,2,4,256] ) ) {
> $fatal_error = "PHP Fatal error: {$last_error['message']} in {$last_error['file']} on line {$last_error['line']}";
> } elseif ( file_exists( WP_CONTENT_DIR . '/debug.log' ) ) {
> $lines = array_reverse( file( WP_CONTENT_DIR . '/debug.log', FILE_IGNORE_NEW_LINES ) );
> foreach ( $lines as $line ) {
> if ( strpos( $line, 'PHP Fatal error' ) !== false ) {
> $fatal_error .= trim( $line );
> break;
> }
> }
> }
>
> if ( $fatal_error ) {
> set_transient( 'wp_last_fatal_error', $fatal_error, 300 );
> }
> }
- No new dependencies
- Fully backward compatible
- Uses existing
WP_DEBUGguard
---
### Ready to open PR:
I can open a PR on GitHub (wordpress/wordpress-develop) if you'd like.
Let me know if you want:
- Screenshots of the email
- Unit test draft
- Alternative using a temp file (fallback)
Happy to iterate.
Keywords: has-patch, needs-testing, administration, php-compatibility
This ticket was mentioned in PR #10445 on WordPress/wordpress-develop by @tlloancy.
11 months ago
#17
- Keywords has-patch added; needs-patch removed
Admins must know *why* an update failed — especially when rollback hides the crash.
### Changes
- Captures fatal error in
has_fatal_error()viaerror_get_last()ordebug.log - Stores in global transient
wp_last_fatal_error - Adds to failure email in English
- Backward log reading → 0 RAM, 0 crash, even on 400MB+
debug.log
### Proof (tested on 4MB log):
`text
--- file() + array_reverse() ---
OK - Temps: 0.003s | Mémoire: +9 Mo
--- Backward reading (this patch) ---
OK - Temps: 0s | Mémoire: +0 Mo
Ready for:
Review
Unit tests (can draft if needed)
Merge
Trac ticket: https://core.trac.wordpress.org/ticket/64155
Fixes #64155
Keywords: has-patch, needs-testing, administration, php-compatibility
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
#18
@
11 months ago
@afragen Thank you!
PR is live:
https://github.com/WordPress/wordpress-develop/pull/10445
This is my first time submitting a PR to WordPress Core — I followed the GitHub PR guidelines and tested thoroughly.
Happy to make any adjustments or add unit tests if needed.
@afragen commented on PR #10445:
11 months ago
#19
Just letting you know I’ll review and comment here.
#20
@
11 months ago
WP_CONTENT_DIR . '/debug.log' is just a default path. Suggest first checking ini_get( 'error_log' ), wich may return empty string, by
} elseif ( file_exists( ini_get( 'error_log ) ?: WP_CONTENT_DIR . '/debug.log' ) ) {
#21
@
11 months ago
@afragen Thank you! I'm excited for the review.
@knutsp Great catch — you're absolutely right!
### Updated patch:
} else {
$log_file = ini_get( 'error_log' ) ?: WP_CONTENT_DIR . '/debug.log';
if ( $log_file && file_exists( $log_file ) && is_readable( $log_file ) ) {
// ... same backward reading logic
}
}
Checks ini_get('error_log') first (user-defined path)
Falls back to WP_CONTENT_DIR . '/debug.log'
Avoids file_exists() → no warning
Pushing it now
#22
@
11 months ago
@tlloancy I’m working in the hospital a bunch for the next week so I may be a bit.
I have an old plugin that might have related code. It scrapes the error log looking for specific text and flushes the cache. Maybe it can give you some ideas. Even ideas about what not to do 😉
https://gist.github.com/afragen/d46d3cc2c7e07d99f921560dfbb70246
#23
@
11 months ago
@afragen No rush — hope you're doing well at the hospital. Take care!
Thanks for the gist! tail_custom() is clever with adaptive buffer.
My version uses char-by-char backward reading — stops at first fatal, scales to any log size.
PR ready for merge.
Fixes #64155
Props: @westonruter, @knutsp
#24
@
11 months ago
@tlloancy as you can tell there are a number of ways to read in the error log.
@westonruter do you think there’s a benefit to adding a general function to read in the error log and scrape a section of it?
#25
@
11 months ago
- Milestone Awaiting Review → Future Release
- Version 6.8.3 → 6.6
@afragen I think so. It seems like something that has come up repeatedly. But I don't think it necessarily should be a prerequisite for resolving this ticket. It would be a nice bonus though!
#26
@
11 months ago
Thanks @westonruter. I just figured I’d ask as I can certainly see it coming up.
This ticket was mentioned in Slack in #core-test by sajib1223. View the logs.
8 months ago
This ticket was mentioned in Slack in #core-test by juanmaguitar. View the logs.
7 months ago
@tlloancy commented on PR #10445:
8 weeks ago
#30
Hi @westonruter @afragen,
I’ve addressed the previous feedback and rebased the PR.
Is there anything else needed before it can move forward? Happy to make any final adjustments.
Thanks!
@afragen commented on PR #10445:
8 weeks ago
#31
Unfortunately I don't have commit access. The best thing to do is join one of the core Slack meetings and keep asking.
#32
@
8 weeks ago
- Milestone Future Release → 7.2
- Owner set to
- Status new → reviewing
Since this is an enhancement, it's not applicable for 7.1 since we're in beta. I'll self-assign for commit for the 7.2 release cycle.
@westonruter commented on PR #10445:
8 weeks ago
#33
Since this is an enhancement, it's not applicable for 7.1 since we're in beta. I'll self-assign for commit for the 7.2 release cycle.
@tlloancy commented on PR #10445:
8 weeks ago
#34
Since this is an enhancement, it's not applicable for 7.1 since we're in beta. I'll self-assign for commit for the 7.2 release cycle.
Thank you @westonruter for moving this to the 7.2 milestone and self-assigning!
Really appreciate you taking ownership of it. Let me know if there's anything else needed from my side.
Thanks again!
@tlloancy commented on PR #10445:
8 weeks ago
#35
Unfortunately I don't have commit access. The best thing to do is join one of the core Slack meetings and keep asking.
Also thank you @afragen for all the reviews, feedback, and guidance along the way. Really appreciated!
#36
@
3 weeks ago
- Keywords changes-requested added; needs-testing removed
Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/10445
Environment
- WordPress: trunk (7.2-alpha-63166-src)
- PHP: 8.2.29 (php-fpm,
error_logini empty) - Server: nginx 1.29.5
- Database: MySQL 8.4.8
- Browser: n/a (WP-CLI,
DOING_CRONdefined so the updater takes the background-update path) - OS: Ubuntu (WSL2)
- Theme: Twenty Twenty-Five
- MU Plugins:
- Trac 64155 harness (test fixture, see Support Content)
- Plugins:
- Trac 64155 Test Plugin 1.0.0 (test fixture, active)
Steps taken
Reproduction on clean trunk
- Installed a tiny fixture plugin (v1.0.0, active) and an MU plugin that injects a fake v1.0.1 update whose package is a local zip containing a call to an undefined function (fatal on load). The MU plugin also opts the plugin into auto-updates, disables core/theme auto-updates, ignores the VCS checkout check and captures
wp_mail()to a file instead of sending. - With
WP_DEBUG+WP_DEBUG_LOGenabled, triggered the background updater via WP-CLI:wp eval 'define("DOING_CRON", true); wp_update_plugins(); wp_maybe_auto_update();'. - Result on clean trunk: the fatal is detected by the loopback scrape, the plugin is rolled back to 1.0.0, and the "Some plugins have failed to update" email is sent — with no error details at all, even though
has_fatal_error()already received the full error in the scrape response (type, message, file, line, stack trace):The following plugins failed to update. If there was a fatal error in the update, the previously installed version has been restored. - Trac 64155 Test Plugin (from version 1.0.0 to 1.0.1) : https://core.trac.wordpress.org/ticket/64155
debug.log at the same moment:PHP Fatal error: Uncaught Error: Call to undefined function trac_64155_undefined_function() in /var/www/src/wp-content/plugins/trac-64155-test/trac-64155-test.php:7 {"type":"1","message":"Uncaught Error: Call to undefined function trac_64155_undefined_function() in wp-content\/plugins\/trac-64155-test\/trac-64155-test.php:7 ...","file":"wp-content\/plugins\/trac-64155-test\/trac-64155-test.php","line":"7"} The update for 'trac-64155-test' contained a fatal error. The previously installed version has been restored.
With patch applied
- Applied PR #10445 (applies cleanly on trunk) and repeated step 2 in three configurations.
WP_DEBUG = true,WP_DEBUG_LOG = true— works. The failure email now ends with:=== Last fatal PHP error === • [23-Aug-2026 10:19:42 UTC] PHP Fatal error: Uncaught Error: Call to undefined function trac_64155_undefined_function() in /var/www/src/wp-content/plugins/trac-64155-test/trac-64155-test.php:7 ========================================
WP_DEBUG = true,WP_DEBUG_LOG = false— the loopback fatal is no longer written to debug.log, so the patch falls back to reading whatever the log file last contained. With an old debug.log holding one unrelated line, the email reported that line instead of the real error:=== Last fatal PHP error === • stale] PHP Fatal error: STALE unrelated fatal from yesterday in /var/www/src/wp-content/plugins/other-plugin.php:1 ========================================
Two issues: (a) the reported error is unrelated to this update — the real fatal was in the scrape response but never in the log; (b) the first byte of the file is dropped ([stale]→stale]), the backward reader'swhile ( $pos > -filesize() )stops one byte early.WP_DEBUG = false(the default for production sites, and the configuration in the ticket description) — the email is identical to clean trunk. The patch is a no-op here because both the capture inhas_fatal_error()and the output insend_plugin_theme_email()are wrapped inif ( WP_DEBUG ).error_get_last()branch: never taken in any of the runs, as expected — the fatal happens in the loopback HTTP request, not in the updater's own process.- PHPUnit
Tests_Admin_WpAutomaticUpdater:OK (45 tests, 70 assertions)(with the harness MU plugin removed). PHPCS on the changed file: 2 new warnings (in_array()without strict, assignment alignment at lines 1845/1858); clean trunk has 0.
❌ Patch is failing
@tlloancy thanks for the PR — the core idea works, but only when WP_DEBUG and WP_DEBUG_LOG are both on (steps 6–7 above). Since has_fatal_error() already decodes the scrape result into $result with message, file and line (the stack trace is inside message), it looks like the error could be taken from there directly, which would drop the error_get_last() / log-file reading and the WP_DEBUG dependency. Happy to re-test once the PR is updated.
@westonruter flagging since you self-assigned this for 7.2 — marking changes-requested.
Support Content
Fixture plugin v1.0.0 (wp-content/plugins/trac-64155-test/trac-64155-test.php):
<?php /** * Plugin Name: Trac 64155 Test Plugin * Version: 1.0.0 */
Fixture update v1.0.1, zipped as trac-64155-test-1.0.1.zip and placed in wp-content/uploads/:
<?php /** * Plugin Name: Trac 64155 Test Plugin * Version: 1.0.1 */ // Deliberate fatal error introduced by the "update". trac_64155_undefined_function();
MU plugin trac-64155-harness.php:
<?php /** * Plugin Name: Trac 64155 harness (MU) * Description: Injects a fake update for trac-64155-test, forces auto-update, captures the failure email to a file. */ // 1. Pretend an update is available, served from the local uploads dir. add_filter( 'pre_set_site_transient_update_plugins', function ( $transient ) { if ( ! is_object( $transient ) ) { $transient = new stdClass(); } $transient->response['trac-64155-test/trac-64155-test.php'] = (object) array( 'id' => 'local/trac-64155-test', 'slug' => 'trac-64155-test', 'plugin' => 'trac-64155-test/trac-64155-test.php', 'new_version' => '1.0.1', 'url' => 'https://core.trac.wordpress.org/ticket/64155', 'package' => home_url( '/wp-content/uploads/trac-64155-test-1.0.1.zip' ), ); return $transient; } ); // 2. Never auto-update core/themes/translations from this harness; only the fixture plugin. add_filter( 'auto_update_core', '__return_false' ); add_filter( 'auto_update_theme', '__return_false' ); add_filter( 'auto_update_translation', '__return_false' ); // 2b. Allow automatic updates even though the checkout is a git repo, and opt this plugin in. add_filter( 'automatic_updates_is_vcs_checkout', '__return_false' ); add_filter( 'auto_update_plugin', function ( $update, $item ) { return 'trac-64155-test' === $item->slug ? true : $update; }, 10, 2 ); // 3. Capture the email instead of sending it. add_filter( 'pre_wp_mail', function ( $null, $atts ) { file_put_contents( WP_CONTENT_DIR . '/trac-64155-mail.txt', "TO: " . ( is_array( $atts['to'] ) ? implode( ',', $atts['to'] ) : $atts['to'] ) . "\nSUBJECT: {$atts['subject']}\n\n{$atts['message']}\n", FILE_APPEND ); return true; }, 10, 2 );
Trigger (from WP-CLI, after activating the fixture plugin and clearing debug.log). DOING_CRON is required: without it Plugin_Upgrader deactivates the plugin before upgrading and an inactive plugin is never checked for fatal errors, so there is no rollback and no failure email. wp_update_plugins() runs the update check (the harness injects the fake 1.0.1 there) and wp_maybe_auto_update() is the background updater itself. In a wordpress-develop checkout use npm run env:cli -- eval '...'.
wp eval 'define("DOING_CRON", true); delete_site_transient("update_plugins"); wp_update_plugins(); wp_maybe_auto_update();'
cat wp-content/trac-64155-mail.txt
#37
@
3 weeks ago
Hi @sajib1223,
Thanks a lot for the detailed testing report!
I've updated the PR to extract the fatal error details directly from the loopback scrape response ($result), instead of relying on error_get_last() or reading the debug log.
As suggested, this completely removes the dependency on WP_DEBUG / WP_DEBUG_LOG and avoids the stale-log issue. I also fixed the two PHPCS warnings you flagged (strict in_array() and alignment).
Ready for another round of testing!
This ticket was mentioned in Slack in #core by thomas_lloancy. View the logs.
2 weeks ago
#39
@
4 days ago
- Keywords has-test-info added; changes-requested removed
Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/10445 (head 696a89f, rebased on trunk 2026-09-11)
Environment
- WordPress: trunk (7.2-alpha-63166-src,
73ea9ac3c7) - PHP: 8.2.29 (php-fpm)
- Server: nginx 1.27.2
- Database: MySQL 9.7.2
- Browser: n/a (WP-CLI,
DOING_CRONdefined so the updater takes the background-update path) - OS: macOS (Docker)
- Theme: Twenty Twenty-Five
- MU Plugins:
- Trac 64155 harness (test fixture, same as in my previous report)
- Trac 64155 harness B (test fixture, only for step 5, see Support Content)
- Plugins:
- Trac 64155 Test Plugin 1.0.0 (test fixture, active)
- Trac 64155 Test Plugin B 1.0.0 (test fixture, active, only for step 5)
Steps taken
Reproduction on clean trunk
- Same setup as in my previous report: a fixture plugin (v1.0.0, active) plus an MU plugin that injects a fake v1.0.1 update whose package is a local zip that fatals on load, opts the plugin into auto-updates and captures
wp_mail()to a file. - Triggered the background updater via WP-CLI:
wp eval 'define("DOING_CRON", true); delete_site_transient("update_plugins"); wp_update_plugins(); wp_maybe_auto_update();'. - Result on clean trunk: the fatal is detected, the plugin is rolled back to 1.0.0 and the "Some plugins have failed to update" email is sent with no error details at all — still reproducible.
With patch applied
- Applied the updated PR (applies cleanly on current trunk) and repeated step 2 in two configurations:
WP_DEBUG = true,WP_DEBUG_LOG = true— the failure email now ends with the full error from the loopback scrape, including the stack trace:=== Last fatal PHP error === • PHP Fatal error: Uncaught Error: Call to undefined function trac_64155_undefined_function() in wp-content/plugins/trac-64155-test/trac-64155-test.php:7 Stack trace: #0 wp-settings.php(608): include_once() #1 /var/www/wp-config.php(107): require_once('wp...') #2 wp-load.php(55): require_once('/var/www/wp-con...') #3 wp-blog-header.php(13): require_once('wp...') #4 _index.php(17): require('wp...') #5 index.php(19): require_once('_i...') #6 {main} thrown in wp-content/plugins/trac-64155-test/trac-64155-test.php on line 7 ========================================WP_DEBUG = false,WP_DEBUG_LOG = false(the production default, and the configuration from the ticket description) — identical output. Nodebug.logis created, so the previous dependency on the log file is gone.
- Two plugins failing in the same run (added a second fixture plugin B whose update throws
RuntimeException( "Plugin B exploded" )). Both are rolled back and both are listed under "The following plugins failed to update", but the "Last fatal PHP error" section only contains plugin B's error; plugin A's error is lost because the singlewp_updater_last_fatal_errortransient is overwritten by eachhas_fatal_error()call. The section also does not say which plugin the error belongs to (it is only inferable from the file path in the message). - Not covered: multisite, the
mixedemail type (one success + one fatal), theerror_get_last()path — the patch no longer uses it. - PHPUnit
Tests_Admin_WpAutomaticUpdater:OK (45 tests, 70 assertions)(fixtures removed). PHPCS on the changed file: 0 errors, 0 warnings (the two warnings from the previous round are fixed).
✅ Patch is solving the problem
@tlloancy thanks for the update — the error now comes straight from the scrape result and it works regardless of WP_DEBUG, which fixes both issues from my previous report. Removing changes-requested.
One thing for the reviewers to consider (step 5): when more than one plugin fatals in the same run only the last error survives. Storing the errors keyed by plugin (e.g. an array in the transient, or on the failed-update result object so send_plugin_theme_email() can print each error under its plugin line) would cover that. Also a small note: the section is appended after the auto_plugin_theme_update_email filter has run, so filters cannot see or customise it.
Support Content
Fixtures for plugin A and the harness MU plugin are in my previous report (unchanged). For step 5, a second plugin was added:
wp-content/plugins/trac-64155-test-b/trac-64155-test-b.php (v1.0.0) is the same header-only plugin as A with Plugin Name: Trac 64155 Test Plugin B. The v1.0.1 zip (trac-64155-test-b-1.0.1.zip in wp-content/uploads/) contains:
<?php /** * Plugin Name: Trac 64155 Test Plugin B * Version: 1.0.1 */ // Deliberate fatal error introduced by the "update" of plugin B. throw new RuntimeException( "Plugin B exploded" );
MU plugin trac-64155-harness-b.php:
<?php /** * Plugin Name: Trac 64155 harness B (MU) * Description: Injects a second fake update (plugin B) that also fatals. */ add_filter( 'pre_set_site_transient_update_plugins', function ( $transient ) { if ( ! is_object( $transient ) ) { $transient = new stdClass(); } $transient->response['trac-64155-test-b/trac-64155-test-b.php'] = (object) array( 'id' => 'local/trac-64155-test-b', 'slug' => 'trac-64155-test-b', 'plugin' => 'trac-64155-test-b/trac-64155-test-b.php', 'new_version' => '1.0.1', 'url' => 'https://core.trac.wordpress.org/ticket/64155', 'package' => home_url( '/wp-content/uploads/trac-64155-test-b-1.0.1.zip' ), ); return $transient; }, 20 ); add_filter( 'auto_update_plugin', function ( $update, $item ) { return 'trac-64155-test-b' === $item->slug ? true : $update; }, 10, 2 );
Trigger (both plugins active, same as before):
wp eval 'define("DOING_CRON", true); delete_site_transient("update_plugins"); wp_update_plugins(); wp_maybe_auto_update();'
cat wp-content/trac-64155-mail.txt
#40
@
4 days ago
Thanks again for the detailed testing report, @sajib1223 ! really appreciate the effort on the multi-plugin scenario.
Pushed a new commit addressing both points:
has_fatal_error() now accepts the plugin slug and stores errors in an array keyed by slug (instead of a single overwritten string), so when several plugins fatal in the same run, each error is preserved and listed under its own plugin in the email.
The "Last fatal PHP error" section is now built before the auto_plugin_theme_update_email filter runs, so filters can see and modify it like the rest of the email body.
Would appreciate a re-test on the two-plugin scenario whenever you have bandwidth. Let me know if anything else needs adjusting.
This ticket was mentioned in Slack in #core by thomas_lloancy. View the logs.
4 days ago
#42
@
33 hours ago
Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/10445 (head f5434c4, 2026-09-13)
Environment
- WordPress: trunk (7.2-alpha-63166-src,
ba9d19e41e) - PHP: 8.2.29 (php-fpm)
- Server: nginx 1.27.2
- Database: MySQL 9.7.2
- Browser: n/a (WP-CLI,
DOING_CRONdefined so the updater takes the background-update path) - OS: macOS (Docker)
- Theme: Twenty Twenty-Five
- MU Plugins:
- Plugins:
- Trac 64155 Test Plugin 1.0.0 (test fixture, active)
- Trac 64155 Test Plugin B 1.0.0 (test fixture, active)
Steps taken
Reproduction on clean trunk
- Same setup as 39: two fixture plugins (A and B, both v1.0.0, active) plus MU plugins that inject fake v1.0.1 updates whose packages are local zips, opt both plugins into auto-updates and capture
wp_mail()to a file. Plugin A's update calls an undefined function, plugin B's throws aRuntimeException. - Triggered the background updater via WP-CLI:
wp eval 'define("DOING_CRON", true); delete_site_transient("update_plugins"); wp_update_plugins(); wp_maybe_auto_update();'. - On clean trunk both plugins are rolled back to 1.0.0 and the failure email lists both, with no error details — still reproducible (same as 39).
With patch applied
- Applied the updated PR (applies cleanly on current trunk). Note for anyone re-running: the updater remembers failure emails already sent per plugin version in the
auto_plugin_theme_update_emailsoption, so after a first run the email is skipped as a duplicate — delete that option between runs. - Two plugins failing,
WP_DEBUG = true— the email now lists both errors, each prefixed with its plugin slug (stack traces trimmed here, they are included in full):=== Last fatal PHP error === • [trac-64155-test] PHP Fatal error: Uncaught Error: Call to undefined function trac_64155_undefined_function() in wp-content/plugins/trac-64155-test/trac-64155-test.php:7 Stack trace: … thrown in wp-content/plugins/trac-64155-test/trac-64155-test.php on line 7 • [trac-64155-test-b] PHP Fatal error: Uncaught RuntimeException: Plugin B exploded in wp-content/plugins/trac-64155-test-b/trac-64155-test-b.php:7 Stack trace: … thrown in wp-content/plugins/trac-64155-test-b/trac-64155-test-b.php on line 7 ========================================
- Two plugins failing,
WP_DEBUG = false,WP_DEBUG_LOG = false— identical output, nodebug.logcreated. - Mixed run (plugin A fatals, plugin B's 1.0.1 is clean),
WP_DEBUG = false— themixedemail lists A under "failed", B under "now up to date", and the fatal section contains only A's error. B stays at 1.0.1. - Filter visibility: an MU plugin hooked to
auto_plugin_theme_update_emailand checked the body. In all runs (failandmixed) the "Last fatal PHP error" section was already present in$email['body']inside the filter, and the filter's modification of the heading made it into the sent email. So the section is now filterable. - Not covered: multisite, theme fatals (themes do not go through
has_fatal_error()), more than one run within the transient's 5-minute lifetime when the email is suppressed as a duplicate (step 4) — in that case the stored errors are carried over into the next failure email, which seems acceptable given the expiry. - PHPUnit
Tests_Admin_WpAutomaticUpdater:OK (45 tests, 70 assertions)(fixtures removed). PHPCS on the changed file: 0 errors, 1 warning —Generic.Formatting.MultipleStatementAlignmenton the$errors[ $slug_key ] = $fatal_error;assignment (line 1862), auto-fixable withphpcbf. Clean trunk has 0.
✅ Patch is solving the problem
@tlloancy thanks — both points from 39 are addressed: every failing plugin's error is kept and labelled with its slug, and the section is visible to the auto_plugin_theme_update_email filter. Only the PHPCS alignment warning is left (step 10).
Support Content
Fixtures for plugins A and B and both harness MU plugins are in 36 and 39 (unchanged). For step 7, plugin B's 1.0.1 zip was replaced with a clean one (same header-only file as v1.0.0 with Version: 1.0.1).
MU plugin trac-64155-filter-probe.php (step 8):
<?php /** * Plugin Name: Trac 64155 filter probe (MU) * Description: Records whether the fatal-error section is visible inside the auto_plugin_theme_update_email filter. */ add_filter( 'auto_plugin_theme_update_email', function ( $email, $type ) { $seen = str_contains( $email['body'], 'Last fatal PHP error' ) ? 'VISIBLE' : 'NOT VISIBLE'; file_put_contents( WP_CONTENT_DIR . '/trac-64155-filter-probe.txt', "type={$type}: fatal section {$seen} in filter\n", FILE_APPEND ); $email['body'] = str_replace( '=== Last fatal PHP error ===', '=== Last fatal PHP error (modified by filter) ===', $email['body'] ); return $email; }, 10, 2 );
Trigger (both plugins active):
wp option delete auto_plugin_theme_update_emails
wp eval 'define("DOING_CRON", true); delete_site_transient("update_plugins"); wp_update_plugins(); wp_maybe_auto_update();'
cat wp-content/trac-64155-mail.txt
cat wp-content/trac-64155-filter-probe.txt
#43
@
29 hours ago
Ok lol so i didn't know about this one. You either align the equals or it seems if you skip a line between them now it's ignored by phpcs.
What i do is add spaces, hoping that now i won't break any 80 characters width rules or else.
(Talking about Generic.Formatting.MultipleStatementAlignment on the $errors[ $slug_key ] = $fatal_error;)
Link to the commit:
Thanks @sajib1223 !
Cheers
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
To provide additional context for this enhancement request, I've observed a specific case in my own custom/low-usage plugin, SlideCraft Reborn (a slider creation tool for WordPress), where a cron task failed silently during a plugin update attempt. The issue stemmed from a call to
wp_cache_flush()in the plugin's code, which interrupted the cron job without triggering any detectable error in WordPress's core mechanisms.This silent failure went completely unnoticed in the update process: no entry appeared in the debug log (
wp-content/debug.log), and the email notification for the failed update provided zero details beyond the basic failure message. Since SlideCraft Reborn is a niche plugin with very few installations (almost no users), there are no community reports or widespread discussions about this, making it even harder to diagnose without manual deep dives into server logs or code.This underscores the need for enhanced error reporting, such as automatically including a stack trace in:
WP_DEBUG_LOGis enabled).Without this, developers of lesser-known plugins (or custom ones) are left guessing, which can lead to overlooked security/maintenance issues.
Steps to Reproduce (on a test site with SlideCraft Reborn installed):
WP_DEBUGandWP_DEBUG_LOGinwp-config.php.wp_cache_flush()call, e.g., during a cron-related hook).wp cron testor server monitoring).If helpful, I can provide a minimal code snippet from SlideCraft Reborn demonstrating the
wp_cache_flush()trigger, or set up a test environment for reproduction.This real-world example from a low-profile plugin supports prioritizing the proposed enhancement to make WordPress more robust for all extension developers.