28use Wikimedia\NormalizedException\INormalizedException;
32use Wikimedia\Services\RecursiveServiceDependencyException;
57 private const FATAL_ERROR_TYPES = [
73 private static $logExceptionBacktrace =
true;
80 private static $propagateErrors;
90 bool $logExceptionBacktrace =
true,
91 bool $propagateErrors =
true
93 self::$logExceptionBacktrace = $logExceptionBacktrace;
94 self::$propagateErrors = $propagateErrors;
109 set_exception_handler( [ self::class,
'handleUncaughtException' ] );
113 set_error_handler( [ self::class,
'handleError' ] );
118 self::$reservedMemory = str_repeat(
' ', 16384 );
119 register_shutdown_function( [ self::class,
'handleFatalError' ] );
125 protected static function report( Throwable $e ) {
128 if ( $e instanceof
MWException && $e->hasOverriddenHandler() ) {
134 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
136 }
catch ( Throwable $e2 ) {
140 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW, $e2 );
149 private static function rollbackPrimaryChanges() {
150 if ( !MediaWikiServices::hasInstance() ) {
156 $services = MediaWikiServices::getInstance();
157 $lbFactory = $services->peekService(
'DBLoadBalancerFactory' );
158 '@phan-var LBFactory $lbFactory';
169 $lbFactory->rollbackPrimaryChanges( __METHOD__ );
170 $lbFactory->flushPrimarySessions( __METHOD__ );
177 self::logException( $e, self::CAUGHT_BY_HANDLER );
192 $catcher = self::CAUGHT_BY_OTHER
194 self::rollbackPrimaryChanges();
196 self::logException( $e, $catcher );
206 self::handleException( $e, self::CAUGHT_BY_HANDLER );
210 register_shutdown_function(
236 public static function handleException( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
237 self::rollbackPrimaryChangesAndLog( $e, $catcher );
263 if ( defined(
'E_STRICT' ) && $level == @constant(
'E_STRICT' ) ) {
264 $level = E_USER_NOTICE;
285 case E_COMPILE_WARNING:
286 $prefix =
'PHP Warning: ';
287 $severity = LogLevel::ERROR;
290 $prefix =
'PHP Notice: ';
291 $severity = LogLevel::ERROR;
295 $prefix =
'PHP Notice: ';
296 $severity = LogLevel::WARNING;
300 $prefix =
'PHP Warning: ';
301 $severity = LogLevel::WARNING;
304 $prefix =
'PHP Deprecated: ';
305 $severity = LogLevel::WARNING;
307 case E_USER_DEPRECATED:
308 $prefix =
'PHP Deprecated: ';
309 $severity = LogLevel::WARNING;
310 $real = MWDebug::parseCallerDescription( $message );
315 $file = $real[
'file'];
316 $line = $real[
'line'];
317 $message = $real[
'message'];
321 $prefix =
'PHP Unknown error: ';
322 $severity = LogLevel::ERROR;
327 $e =
new ErrorException( $prefix . $message, 0, $level, $file, $line );
328 self::logError( $e, $severity, self::CAUGHT_BY_HANDLER );
333 return !( self::$propagateErrors || ini_get(
'track_errors' ) );
353 self::$reservedMemory =
null;
355 $lastError = error_get_last();
356 if ( $lastError ===
null ) {
360 $level = $lastError[
'type'];
361 $message = $lastError[
'message'];
362 $file = $lastError[
'file'];
363 $line = $lastError[
'line'];
365 if ( !in_array( $level, self::FATAL_ERROR_TYPES ) ) {
372 '[{reqId}] {exception_url} PHP Fatal Error',
373 ( $line || $file ) ?
' from' :
'',
374 $line ?
" line $line" :
'',
375 ( $line && $file ) ?
' of' :
'',
376 $file ?
" $file" :
'',
379 $msg = implode(
'', $msgParts );
382 if ( preg_match(
"/Class '\w+' not found/", $message ) ) {
387MediaWiki or an installed extension
requires this class but it is not embedded directly in
MediaWiki's git repository and must be installed separately by the end user.
389Please see <a href=
"https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a>
for help on installing the required components.
394 $e =
new ErrorException(
"PHP Fatal Error: {$message}", 0, $level, $file, $line );
395 $logger = LoggerFactory::getInstance(
'exception' );
396 $logger->error( $msg, self::getLogContext( $e, self::CAUGHT_BY_HANDLER ) );
412 $from =
'from ' . $e->getFile() .
'(' . $e->getLine() .
')' .
"\n";
413 return $from . self::prettyPrintTrace( self::getRedactedTrace( $e ) );
428 foreach ( $trace as $level => $frame ) {
429 if ( isset( $frame[
'file'] ) && isset( $frame[
'line'] ) ) {
430 $text .=
"{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
436 $text .=
"{$pad}#{$level} [internal function]: ";
439 if ( isset( $frame[
'class'] ) && isset( $frame[
'type'] ) && isset( $frame[
'function'] ) ) {
440 $text .= $frame[
'class'] . $frame[
'type'] . $frame[
'function'];
442 $text .= $frame[
'function'] ??
'NO_FUNCTION_GIVEN';
445 if ( isset( $frame[
'args'] ) ) {
446 $text .=
'(' . implode(
', ', $frame[
'args'] ) .
")\n";
453 $text .=
"{$pad}#{$level} {main}";
470 return static::redactTrace( $e->getTrace() );
484 return array_map(
static function ( $frame ) {
485 if ( isset( $frame[
'args'] ) ) {
486 $frame[
'args'] = array_map(
'get_debug_type', $frame[
'args'] );
503 return WebRequest::getGlobalRequestURL();
517 $id = WebRequest::getRequestId();
518 $type = get_class( $e );
519 $message = $e->getMessage();
520 $url = self::getURL() ?:
'[no req]';
523 $message =
"A database query error has occurred. Did you forget to run"
524 .
" your application's database schema updater after upgrading"
525 .
" or after adding a new extension?\n\nPlease see"
526 .
" https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Upgrading and"
527 .
" https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:How_to_debug"
528 .
" for more information.\n\n"
532 return "[$id] $url $type: $message";
545 if ( $e instanceof INormalizedException ) {
546 $message = $e->getNormalizedMessage();
548 $message = $e->getMessage();
550 if ( !$e instanceof ErrorException ) {
555 $message = get_class( $e ) .
": $message";
558 return "[{reqId}] {exception_url} $message";
566 $reqId = WebRequest::getRequestId();
567 $type = get_class( $e );
568 return '[' . $reqId .
'] '
569 . gmdate(
'Y-m-d H:i:s' ) .
': '
570 .
'Fatal exception of type "' . $type .
'"';
585 public static function getLogContext( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
588 'exception_url' => self::getURL() ?:
'[no req]',
595 'reqId' => WebRequest::getRequestId(),
596 'caught_by' => $catcher
598 if ( $e instanceof INormalizedException ) {
599 $context += $e->getMessageContext();
618 $catcher = self::CAUGHT_BY_OTHER
621 'id' => WebRequest::getRequestId(),
622 'type' => get_class( $e ),
623 'file' => $e->getFile(),
624 'line' => $e->getLine(),
625 'message' => $e->getMessage(),
626 'code' => $e->getCode(),
627 'url' => self::getURL() ?:
null,
628 'caught_by' => $catcher
631 if ( $e instanceof ErrorException &&
632 ( error_reporting() & $e->getSeverity() ) === 0
635 $data[
'suppressed'] =
true;
638 if ( self::$logExceptionBacktrace ) {
639 $data[
'backtrace'] = self::getRedactedTrace( $e );
642 $previous = $e->getPrevious();
643 if ( $previous !==
null ) {
644 $data[
'previous'] = self::getStructuredExceptionData( $previous, $catcher );
708 $catcher = self::CAUGHT_BY_OTHER
710 return FormatJson::encode(
711 self::getStructuredExceptionData( $e, $catcher ),
730 $catcher = self::CAUGHT_BY_OTHER,
733 if ( !( $e instanceof
MWException ) || $e->isLoggable() ) {
734 $logger = LoggerFactory::getInstance(
'exception' );
735 $context = self::getLogContext( $e, $catcher );
737 $context[
'extraData'] = $extraData;
740 self::getLogNormalMessage( $e ),
744 $json = self::jsonSerializeException( $e,
false, FormatJson::ALL_OK, $catcher );
745 if ( $json !==
false ) {
746 $logger = LoggerFactory::getInstance(
'exception-json' );
747 $logger->error( $json, [
'private' =>
true ] );
750 self::callLogExceptionHook( $e,
false );
761 private static function logError(
767 $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
774 $channel =
'silenced-error';
775 $level = LogLevel::DEBUG;
779 $logger = LoggerFactory::getInstance( $channel );
782 self::getLogNormalMessage( $e ),
783 self::getLogContext( $e, $catcher )
786 self::callLogExceptionHook( $e, $suppressed );
795 private static function callLogExceptionHook( Throwable $e,
bool $suppressed ) {
801 if ( !MediaWikiServices::hasInstance() ) {
805 (
new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
806 ->onLogException( $e, $suppressed );
807 }
catch ( RecursiveServiceDependencyException $e ) {
wfIsCLI()
Check if we are running from the commandline.
Handler class for MWExceptions.
static getLogContext(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a PSR-3 log event context from a Throwable.
const CAUGHT_BY_HANDLER
Error caught and reported by this exception handler.
static handleError( $level, $message, $file=null, $line=null)
Handler for set_error_handler() callback notifications.
static rollbackPrimaryChangesAndLog(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Roll back any open database transactions and log the stack trace of the throwable.
static installHandler(bool $logExceptionBacktrace=true, bool $propagateErrors=true)
Install handlers with PHP.
const CAUGHT_BY_OTHER
Error reported by direct logException() call.
static getStructuredExceptionData(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a structured representation of a Throwable.
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
static report(Throwable $e)
Report a throwable to the user.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
static getPublicLogMessage(Throwable $e)
static getRedactedTrace(Throwable $e)
Return a copy of a throwable's backtrace as an array.
static handleUncaughtException(Throwable $e)
Callback to use with PHP's set_exception_handler.
static prettyPrintTrace(array $trace, $pad='')
Generate a string representation of a stacktrace.
static string null $reservedMemory
static jsonSerializeException(Throwable $e, $pretty=false, $escaping=0, $catcher=self::CAUGHT_BY_OTHER)
Serialize a Throwable object to JSON.
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
const CAUGHT_BY_ENTRYPOINT
Error caught and reported by a script entry point.
static redactTrace(array $trace)
Redact a stacktrace generated by Throwable::getTrace(), debug_backtrace() or similar means.
static handleFatalError()
Callback used as a registered shutdown function.
static getLogNormalMessage(Throwable $e)
Get a normalised message for formatting with PSR-3 log event context.
static getURL()
If the exception occurred in the course of responding to a request, returns the requested URL.
static handleException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Exception handler which simulates the appropriate catch() handling: