17use Wikimedia\NormalizedException\INormalizedException;
21use Wikimedia\Services\RecursiveServiceDependencyException;
46privateconst FATAL_ERROR_TYPES = [
53// E.g. "Catchable fatal error: Argument X must be Y, null given" 62privatestatic $logExceptionBacktrace =
true;
69privatestatic $propagateErrors;
79bool $logExceptionBacktrace =
true,
80bool $propagateErrors =
true 82 self::$logExceptionBacktrace = $logExceptionBacktrace;
83 self::$propagateErrors = $propagateErrors;
86// * Exception objects that were explicitly thrown but not 87// caught anywhere in the application. This is rare given those 88// would normally be caught at a high-level like MediaWiki::run (index.php), 89// api.php, or ResourceLoader::respond (load.php). These high-level 90// catch clauses would then call MWExceptionHandler::logException 91// or MWExceptionHandler::handleException. 92// If they are not caught, then they are handled here. 93// * Error objects for issues that would historically 94// cause fatal errors but may now be caught as Throwable (not Exception). 95// Same as previous case, but more common to bubble to here instead of 96// caught locally because they tend to not be safe to recover from. 97// (e.g. argument TypeError, division by zero, etc.) 98 set_exception_handler( self::handleUncaughtException( ... ) );
100// This catches recoverable errors (e.g. PHP Notice, PHP Warning, PHP Error) that do not 101// interrupt execution in any way. We log these in the background and then continue execution. 102 set_error_handler( self::handleError( ... ) );
104// This catches fatal errors for which no Throwable is thrown, 105// including Out-Of-Memory and Timeout fatals. 106// Reserve 16k of memory so we can report OOM fatals. 107 self::$reservedMemory = str_repeat(
' ', 16384 );
108 register_shutdown_function( self::handleFatalError( ... ) );
114protectedstaticfunctionreport( Throwable $e ) {
116// Try and show the exception prettily, with the normal skin infrastructure 117if ( $e instanceof
MWException && $e->hasOverriddenHandler() ) {
118// Delegate to MWException until all subclasses are handled by 119// MWExceptionRenderer and MWException::report() has been 125 }
catch ( Throwable $e2 ) {
126// Exception occurred from within exception handler 127// Show a simpler message for the original exception, 128// don't try to invoke report() 138privatestaticfunction rollbackPrimaryChanges() {
140// MediaWiki isn't fully initialized yet, it's not safe to access services. 141// This also means that there's nothing to roll back yet. 146 $lbFactory = $services->peekService(
'DBLoadBalancerFactory' );
147'@phan-var LBFactory $lbFactory';
/* @var LBFactory $lbFactory */ 149// There's no need to roll back transactions if the LBFactory is 150// disabled or hasn't been created yet 154// Roll back DBs to avoid transaction notices. This might fail 155// to roll back some databases due to connection issues or exceptions. 156// However, any sensible DB driver will roll back implicitly anyway. 158 $lbFactory->rollbackPrimaryChanges( __METHOD__ );
159 $lbFactory->flushPrimarySessions( __METHOD__ );
160 }
catch ( DBError $e ) {
161// If the DB is unreachable, rollback() will throw an error 162// and the error report() method might need messages from the DB, 163// which would result in an exception loop. PHP may escalate such 164// errors to "Exception thrown without a stack frame" fatals, but 165// it's better to be explicit here. 181 $catcher = self::CAUGHT_BY_OTHER
183 self::rollbackPrimaryChanges();
197// Make sure we don't claim success on exit for CLI scripts (T177414) 199 register_shutdown_function(
200staticfunction (): never {
222publicstaticfunctionhandleException( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
247// E_STRICT is deprecated since PHP 8.4 (T375707). 248// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged 249if ( defined(
'E_STRICT' ) && $level == @constant(
'E_STRICT' ) ) {
250 $level = E_USER_NOTICE;
253// Map PHP error constant to a PSR-3 severity level. 254// Avoid use of "DEBUG" or "INFO" levels, unless the 255// error should evade error monitoring and alerts. 257// To decide the log level, ask yourself: "Has the 258// program's behaviour diverged from what the written 261// For example, use of a deprecated method or violating a strict standard 262// has no impact on functional behaviour (Warning). On the other hand, 263// accessing an undefined variable makes behaviour diverge from what the 264// author intended/expected. PHP recovers from an undefined variables by 265// yielding null and continuing execution, but it remains a change in 266// behaviour given the null was not part of the code and is likely not 271case E_COMPILE_WARNING:
272 $prefix =
'PHP Warning: ';
273 $severity = LogLevel::ERROR;
276 $prefix =
'PHP Notice: ';
277 $severity = LogLevel::ERROR;
280// Used by wfWarn(), MWDebug::warning() 281 $prefix =
'PHP Notice: ';
282 $severity = LogLevel::WARNING;
285// Used by wfWarn(), MWDebug::warning() 286 $prefix =
'PHP Warning: ';
287 $severity = LogLevel::WARNING;
290 $prefix =
'PHP Deprecated: ';
291 $severity = LogLevel::WARNING;
293case E_USER_DEPRECATED:
294 $prefix =
'PHP Deprecated: ';
295 $severity = LogLevel::WARNING;
296 $real = MWDebug::parseCallerDescription( $message );
298// Used by wfDeprecated(), MWDebug::deprecated() 299// Apply caller offset from wfDeprecated() to the native error. 300// This makes errors easier to aggregate and find in e.g. Kibana. 301 $file = $real[
'file'];
302 $line = $real[
'line'];
303 $message = $real[
'message'];
307 $prefix =
'PHP Unknown error: ';
308 $severity = LogLevel::ERROR;
312// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal False positive 313 $e =
new ErrorException( $prefix . $message, 0, $level, $file, $line );
314 self::logError( $e, $severity, self::CAUGHT_BY_HANDLER );
316// If $propagateErrors is true return false so PHP shows/logs the error normally. 317return !self::$propagateErrors;
334// Free reserved memory so that we have space to process OOM 338 $lastError = error_get_last();
339if ( $lastError ===
null ) {
343 $level = $lastError[
'type'];
344 $message = $lastError[
'message'];
345 $file = $lastError[
'file'];
346 $line = $lastError[
'line'];
348if ( !in_array( $level, self::FATAL_ERROR_TYPES ) ) {
349// Only interested in fatal errors, others should have been 350// handled by MWExceptionHandler::handleError 355'[{reqId}] {exception_url} PHP Fatal Error',
356 ( $line || $file ) ?
' from' :
'',
357 $line ?
" line $line" :
'',
358 ( $line && $file ) ?
' of' :
'',
362 $msg = implode(
'', $msgParts );
364// Look at message to see if this is a class not found failure (Class 'foo' not found) 365if ( preg_match(
"/Class '\w+' not found/", $message ) ) {
366// phpcs:disable Generic.Files.LineLength 370MediaWiki or an installed extension
requiresthisclassbut it is not embedded directly in
MediaWiki's git repository and must be installed separately by the end user.
372Please see <a href=
"https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a>
for help on installing the required components.
377 $e =
new ErrorException(
"PHP Fatal Error: {$message}", 0, $level, $file, $line );
378$logger = LoggerFactory::getInstance(
'exception' );
379$logger->error( $msg, self::getLogContext( $e, self::CAUGHT_BY_HANDLER ) );
393 $from =
'from ' . $e->getFile() .
'(' . $e->getLine() .
')' .
"\n";
394return $from . self::prettyPrintTrace( self::getRedactedTrace( $e ) );
409foreach ( $trace as $level => $frame ) {
410if ( isset( $frame[
'file'] ) && isset( $frame[
'line'] ) ) {
411 $text .=
"{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
413// 'file' and 'line' are unset for calls from C code 414// (T57634) This matches behaviour of 415// Throwable::getTraceAsString to instead display "[internal 417 $text .=
"{$pad}#{$level} [internal function]: ";
420if ( isset( $frame[
'class'] ) && isset( $frame[
'type'] ) && isset( $frame[
'function'] ) ) {
421 $text .= $frame[
'class'] . $frame[
'type'] . $frame[
'function'];
423 $text .= $frame[
'function'] ??
'NO_FUNCTION_GIVEN';
426if ( isset( $frame[
'args'] ) ) {
427 $text .=
'(' . implode(
', ', $frame[
'args'] ) .
")\n";
434 $text .=
"{$pad}#{$level} {main}";
451return static::redactTrace( $e->getTrace() );
465return array_map(
staticfunction ( $frame ) {
466if ( isset( $frame[
'args'] ) ) {
467 $frame[
'args'] = array_map(
'get_debug_type', $frame[
'args'] );
484return WebRequest::getGlobalRequestURL();
498 $id = WebRequest::getRequestId();
499 $type = get_class( $e );
500 $message = $e->getMessage();
501$url = self::getURL() ?:
'[no req]';
504 $message =
"A database query error has occurred. Did you forget to run" 505 .
" your application's database schema updater after upgrading" 506 .
" or after adding a new extension?\n\nPlease see" 507 .
" https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Upgrading and" 508 .
" https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:How_to_debug" 509 .
" for more information.\n\n" 513return"[$id] $url $type: $message";
526if ( $e instanceof INormalizedException ) {
527 $message = $e->getNormalizedMessage();
529 $message = $e->getMessage();
531if ( !$e instanceof ErrorException ) {
532// ErrorException is something we use internally to represent 533// PHP errors (runtime warnings that aren't thrown or caught), 534// don't bother putting it in the logs. Let the log message 535// lead with "PHP Warning: " instead (see ::handleError). 536 $message = get_class( $e ) .
": $message";
539return"[{reqId}] {exception_url} $message";
547 $reqId = WebRequest::getRequestId();
548 $type = get_class( $e );
549return'[' . $reqId .
'] ' 550 . gmdate(
'Y-m-d H:i:s' ) .
': ' 551 .
'Fatal exception of type "' . $type .
'"';
566publicstaticfunctiongetLogContext( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
569'exception_url' => self::getURL() ?:
'[no req]',
570// The reqId context key use the same familiar name and value as the top-level field 571// provided by LogstashFormatter. However, formatters are configurable at run-time, 572// and their top-level fields are logically separate from context keys and cannot be, 573// substituted in a message, hence set explicitly here. For WMF users, these may feel, 574// like the same thing due to Monolog V0 handling, which transmits "fields" and "context", 575// in the same JSON object (after message formatting). 576'reqId' => WebRequest::getRequestId(),
577'caught_by' => $catcher
579if ( $e instanceof INormalizedException ) {
580 $context += $e->getMessageContext();
599 $catcher = self::CAUGHT_BY_OTHER
602'id' => WebRequest::getRequestId(),
603'type' => get_class( $e ),
604'file' => $e->getFile(),
605'line' => $e->getLine(),
606'message' => $e->getMessage(),
607'code' => $e->getCode(),
608'url' => self::getURL() ?:
null,
609'caught_by' => $catcher
612if ( $e instanceof ErrorException &&
613 ( error_reporting() & $e->getSeverity() ) === 0
615// Flag suppressed errors 616 $data[
'suppressed'] =
true;
619if ( self::$logExceptionBacktrace ) {
620 $data[
'backtrace'] = self::getRedactedTrace( $e );
623 $previous = $e->getPrevious();
624if ( $previous !==
null ) {
625 $data[
'previous'] = self::getStructuredExceptionData( $previous, $catcher );
644 $catcher = self::CAUGHT_BY_OTHER,
647if ( !( $e instanceof
MWException ) || $e->isLoggable() ) {
648 $logger = LoggerFactory::getInstance(
'exception' );
649 $context = self::getLogContext( $e, $catcher );
651 $context[
'extraData'] = $extraData;
654 self::getLogNormalMessage( $e ),
658 self::callLogExceptionHook( $e,
false );
669privatestaticfunction logError(
674// The set_error_handler callback is independent from error_reporting. 675 $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
677// Instead of discarding these entirely, give some visibility (but only 678// when debugging) to errors that were intentionally silenced via 679// the error silencing operator (@) or Wikimedia\AtEase. 680// To avoid clobbering Logstash results, set the level to DEBUG 681// and also send them to a dedicated channel (T193472). 682 $channel =
'silenced-error';
683 $level = LogLevel::DEBUG;
687 $logger = LoggerFactory::getInstance( $channel );
690 self::getLogNormalMessage( $e ),
691 self::getLogContext( $e, $catcher )
694 self::callLogExceptionHook( $e, $suppressed );
703privatestaticfunction callLogExceptionHook( Throwable $e,
bool $suppressed ) {
705// It's possible for the exception handler to be triggered during service container 706// initialization, e.g. if an autoloaded file triggers deprecation warnings. 707// To avoid a difficult-to-debug autoload loop, avoid attempting to initialize the service 708// container here. (T380456). 709// The exception handler is also triggered when autoloading of HookRunner class fails, 710// > Uncaught Error: Class "MediaWiki\HookContainer\HookRunner" not found 711// Avoid use of the not-loaded class here, as that override the real error. 717 ->onLogException( $e, $suppressed );
718 }
catch ( RecursiveServiceDependencyException ) {
719// An error from the HookContainer wiring will lead here (T379125) 725class_alias( MWExceptionHandler::class,
'MWExceptionHandler' );
wfIsCLI()
Check if we are running from the commandline.
Handler class for MWExceptions.
static getStructuredExceptionData(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a structured representation of a Throwable.
static getRedactedTrace(Throwable $e)
Return a copy of a throwable's backtrace as an array.
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
static redactTrace(array $trace)
Redact a stacktrace generated by Throwable::getTrace(), debug_backtrace() or similar means.
const CAUGHT_BY_HANDLER
Error caught and reported by this exception handler.
static getLogContext(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a PSR-3 log event context from a Throwable.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
static report(Throwable $e)
Report a throwable to the user.
static getURL()
If the exception occurred in the course of responding to a request, returns the requested URL.
static handleFatalError()
Callback used as a registered shutdown function.
const CAUGHT_BY_ENTRYPOINT
Error caught and reported by a script entry point.
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
static handleError( $level, $message, $file=null, $line=null)
Handler for set_error_handler() callback notifications.
static getLogNormalMessage(Throwable $e)
Get a normalised message for formatting with PSR-3 log event context.
static handleException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Exception handler which simulates the appropriate catch() handling:
static getPublicLogMessage(Throwable $e)
static installHandler(bool $logExceptionBacktrace=true, bool $propagateErrors=true)
Install handlers with PHP.
static handleUncaughtException(Throwable $e)
Callback to use with PHP's set_exception_handler.
static rollbackPrimaryChangesAndLog(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Roll back any open database transactions and log the stack trace of the throwable.
const CAUGHT_BY_OTHER
Error reported by direct logException() call.
static string null $reservedMemory
static prettyPrintTrace(array $trace, $pad='')
Generate a string representation of a stacktrace.
static output(Throwable $e, $mode, ?Throwable $eNew=null)
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Create PSR-3 logger objects.
Service locator for MediaWiki core services.
static hasInstance()
Returns true if an instance has already been initialized and can be obtained from getInstance().
static getInstance()
Returns the global default instance of the top level service locator.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Database error base class.
Helper trait for implementations \DAO.
LoggerInterface $logger
The logger instance.