MediaWiki  master
MWExceptionHandler.php
Go to the documentation of this file.
1 <?php
23 use Psr\Log\LogLevel;
24 use Wikimedia\NormalizedException\INormalizedException;
27 
34  public const CAUGHT_BY_HANDLER = 'mwe_handler';
36  public const CAUGHT_BY_ENTRYPOINT = 'entrypoint';
38  public const CAUGHT_BY_OTHER = 'other';
39 
41  protected static $reservedMemory;
42 
53  protected static $fatalErrorTypes = [
54  E_ERROR,
55  E_PARSE,
56  E_CORE_ERROR,
57  E_COMPILE_ERROR,
58  E_USER_ERROR,
59 
60  // E.g. "Catchable fatal error: Argument X must be Y, null given"
61  E_RECOVERABLE_ERROR,
62  ];
63 
67  public static function installHandler() {
68  // This catches:
69  // * Exception objects that were explicitly thrown but not
70  // caught anywhere in the application. This is rare given those
71  // would normally be caught at a high-level like MediaWiki::run (index.php),
72  // api.php, or ResourceLoader::respond (load.php). These high-level
73  // catch clauses would then call MWExceptionHandler::logException
74  // or MWExceptionHandler::handleException.
75  // If they are not caught, then they are handled here.
76  // * Error objects for issues that would historically
77  // cause fatal errors but may now be caught as Throwable (not Exception).
78  // Same as previous case, but more common to bubble to here instead of
79  // caught locally because they tend to not be safe to recover from.
80  // (e.g. argument TypeError, division by zero, etc.)
81  set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
82 
83  // This catches recoverable errors (e.g. PHP Notice, PHP Warning, PHP Error) that do not
84  // interrupt execution in any way. We log these in the background and then continue execution.
85  set_error_handler( 'MWExceptionHandler::handleError' );
86 
87  // This catches fatal errors for which no Throwable is thrown,
88  // including Out-Of-Memory and Timeout fatals.
89  // Reserve 16k of memory so we can report OOM fatals.
90  self::$reservedMemory = str_repeat( ' ', 16384 );
91  register_shutdown_function( 'MWExceptionHandler::handleFatalError' );
92  }
93 
98  protected static function report( Throwable $e ) {
99  try {
100  // Try and show the exception prettily, with the normal skin infrastructure
101  if ( $e instanceof MWException ) {
102  // Delegate to MWException until all subclasses are handled by
103  // MWExceptionRenderer and MWException::report() has been
104  // removed.
105  $e->report();
106  } else {
108  }
109  } catch ( Throwable $e2 ) {
110  // Exception occurred from within exception handler
111  // Show a simpler message for the original exception,
112  // don't try to invoke report()
114  }
115  }
116 
126  public static function rollbackPrimaryChangesAndLog(
127  Throwable $e,
128  $catcher = self::CAUGHT_BY_OTHER
129  ) {
130  $services = MediaWikiServices::getInstance();
131  if ( !$services->isServiceDisabled( 'DBLoadBalancerFactory' ) ) {
132  // Rollback DBs to avoid transaction notices. This might fail
133  // to rollback some databases due to connection issues or exceptions.
134  // However, any sane DB driver will rollback implicitly anyway.
135  try {
136  $services->getDBLoadBalancerFactory()->rollbackPrimaryChanges( __METHOD__ );
137  } catch ( DBError $e2 ) {
138  // If the DB is unreacheable, rollback() will throw an error
139  // and the error report() method might need messages from the DB,
140  // which would result in an exception loop. PHP may escalate such
141  // errors to "Exception thrown without a stack frame" fatals, but
142  // it's better to be explicit here.
143  self::logException( $e2, $catcher );
144  }
145  }
146 
147  self::logException( $e, $catcher );
148  }
149 
155  public static function rollbackMasterChangesAndLog(
156  Throwable $e,
157  $catcher = self::CAUGHT_BY_OTHER
158  ) {
159  wfDeprecated( __METHOD__, '1.37' );
160  self::rollbackPrimaryChangesAndLog( $e, $catcher );
161  }
162 
169  public static function handleUncaughtException( Throwable $e ) {
170  self::handleException( $e, self::CAUGHT_BY_HANDLER );
171 
172  // Make sure we don't claim success on exit for CLI scripts (T177414)
173  if ( wfIsCLI() ) {
174  register_shutdown_function(
178  static function () {
179  exit( 255 );
180  }
181  );
182  }
183  }
184 
200  public static function handleException( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
201  self::rollbackPrimaryChangesAndLog( $e, $catcher );
202  self::report( $e );
203  }
204 
219  public static function handleError(
220  $level,
221  $message,
222  $file = null,
223  $line = null
224  ) {
225  global $wgPropagateErrors;
226 
227  // Map PHP error constant to a PSR-3 severity level.
228  // Avoid use of "DEBUG" or "INFO" levels, unless the
229  // error should evade error monitoring and alerts.
230  //
231  // To decide the log level, ask yourself: "Has the
232  // program's behaviour diverged from what the written
233  // code expected?"
234  //
235  // For example, use of a deprecated method or violating a strict standard
236  // has no impact on functional behaviour (Warning). On the other hand,
237  // accessing an undefined variable makes behaviour diverge from what the
238  // author intended/expected. PHP recovers from an undefined variables by
239  // yielding null and continuing execution, but it remains a change in
240  // behaviour given the null was not part of the code and is likely not
241  // accounted for.
242  switch ( $level ) {
243  case E_WARNING:
244  case E_CORE_WARNING:
245  case E_COMPILE_WARNING:
246  $prefix = 'PHP Warning: ';
247  $severity = LogLevel::ERROR;
248  break;
249  case E_NOTICE:
250  $prefix = 'PHP Notice: ';
251  $severity = LogLevel::ERROR;
252  break;
253  case E_USER_NOTICE:
254  // Used by wfWarn(), MWDebug::warning()
255  $prefix = 'PHP Notice: ';
256  $severity = LogLevel::WARNING;
257  break;
258  case E_USER_WARNING:
259  // Used by wfWarn(), MWDebug::warning()
260  $prefix = 'PHP Warning: ';
261  $severity = LogLevel::WARNING;
262  break;
263  case E_STRICT:
264  $prefix = 'PHP Strict Standards: ';
265  $severity = LogLevel::WARNING;
266  break;
267  case E_DEPRECATED:
268  $prefix = 'PHP Deprecated: ';
269  $severity = LogLevel::WARNING;
270  break;
271  case E_USER_DEPRECATED:
272  $prefix = 'PHP Deprecated: ';
273  $severity = LogLevel::WARNING;
274  $real = MWDebug::parseCallerDescription( $message );
275  if ( $real ) {
276  // Used by wfDeprecated(), MWDebug::deprecated()
277  // Apply caller offset from wfDeprecated() to the native error.
278  // This makes errors easier to aggregate and find in e.g. Kibana.
279  $file = $real['file'];
280  $line = $real['line'];
281  $message = $real['message'];
282  }
283  break;
284  default:
285  $prefix = 'PHP Unknown error: ';
286  $severity = LogLevel::ERROR;
287  break;
288  }
289 
290  $e = new ErrorException( $prefix . $message, 0, $level, $file, $line );
291  self::logError( $e, 'error', $severity, self::CAUGHT_BY_HANDLER );
292 
293  // If $wgPropagateErrors is true return false so PHP shows/logs the error normally.
294  // Ignore $wgPropagateErrors if track_errors is set
295  // (which means someone is counting on regular PHP error handling behavior).
296  return !( $wgPropagateErrors || ini_get( 'track_errors' ) );
297  }
298 
313  public static function handleFatalError() {
314  // Free reserved memory so that we have space to process OOM
315  // errors
316  self::$reservedMemory = null;
317 
318  $lastError = error_get_last();
319  if ( $lastError === null ) {
320  return false;
321  }
322 
323  $level = $lastError['type'];
324  $message = $lastError['message'];
325  $file = $lastError['file'];
326  $line = $lastError['line'];
327 
328  if ( !in_array( $level, self::$fatalErrorTypes ) ) {
329  // Only interested in fatal errors, others should have been
330  // handled by MWExceptionHandler::handleError
331  return false;
332  }
333 
335  $msgParts = [
336  '[{reqId}] {exception_url} PHP Fatal Error',
337  ( $line || $file ) ? ' from' : '',
338  $line ? " line $line" : '',
339  ( $line && $file ) ? ' of' : '',
340  $file ? " $file" : '',
341  ": $message",
342  ];
343  $msg = implode( '', $msgParts );
344 
345  // Look at message to see if this is a class not found failure (Class 'foo' not found)
346  if ( preg_match( "/Class '\w+' not found/", $message ) ) {
347  // phpcs:disable Generic.Files.LineLength
348  $msg = <<<TXT
349 {$msg}
350 
351 MediaWiki 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.
352 
353 Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
354 TXT;
355  // phpcs:enable
356  }
357 
358  $e = new ErrorException( "PHP Fatal Error: {$message}", 0, $level, $file, $line );
359  $logger = LoggerFactory::getInstance( 'exception' );
360  $logger->error( $msg, self::getLogContext( $e, self::CAUGHT_BY_HANDLER ) );
361 
362  return false;
363  }
364 
375  public static function getRedactedTraceAsString( Throwable $e ) {
376  $from = 'from ' . $e->getFile() . '(' . $e->getLine() . ')' . "\n";
377  return $from . self::prettyPrintTrace( self::getRedactedTrace( $e ) );
378  }
379 
388  public static function prettyPrintTrace( array $trace, $pad = '' ) {
389  $text = '';
390 
391  $level = 0;
392  foreach ( $trace as $level => $frame ) {
393  if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
394  $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
395  } else {
396  // 'file' and 'line' are unset for calls from C code
397  // (T57634) This matches behaviour of
398  // Throwable::getTraceAsString to instead display "[internal
399  // function]".
400  $text .= "{$pad}#{$level} [internal function]: ";
401  }
402 
403  if ( isset( $frame['class'] ) && isset( $frame['type'] ) && isset( $frame['function'] ) ) {
404  $text .= $frame['class'] . $frame['type'] . $frame['function'];
405  } elseif ( isset( $frame['function'] ) ) {
406  $text .= $frame['function'];
407  } else {
408  $text .= 'NO_FUNCTION_GIVEN';
409  }
410 
411  if ( isset( $frame['args'] ) ) {
412  $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
413  } else {
414  $text .= "()\n";
415  }
416  }
417 
418  $level++;
419  $text .= "{$pad}#{$level} {main}";
420 
421  return $text;
422  }
423 
435  public static function getRedactedTrace( Throwable $e ) {
436  return static::redactTrace( $e->getTrace() );
437  }
438 
449  public static function redactTrace( array $trace ) {
450  return array_map( static function ( $frame ) {
451  if ( isset( $frame['args'] ) ) {
452  $frame['args'] = array_map( static function ( $arg ) {
453  return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
454  }, $frame['args'] );
455  }
456  return $frame;
457  }, $trace );
458  }
459 
467  public static function getURL() {
468  global $wgRequest;
469  if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
470  return false;
471  }
472  return $wgRequest->getRequestURL();
473  }
474 
486  public static function getLogMessage( Throwable $e ) {
487  $id = WebRequest::getRequestId();
488  $type = get_class( $e );
489  $message = $e->getMessage();
490  $url = self::getURL() ?: '[no req]';
491 
492  if ( $e instanceof DBQueryError ) {
493  $message = "A database query error has occurred. Did you forget to run"
494  . " your application's database schema updater after upgrading"
495  . " or after adding a new extension?\n\nPlease see"
496  . " https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Upgrading and"
497  . " https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:How_to_debug"
498  . " for more information.\n\n"
499  . $message;
500  }
501 
502  return "[$id] $url $type: $message";
503  }
504 
514  public static function getLogNormalMessage( Throwable $e ) {
515  if ( $e instanceof INormalizedException ) {
516  $message = $e->getNormalizedMessage();
517  } else {
518  $message = $e->getMessage();
519  }
520  if ( !$e instanceof ErrorException ) {
521  // ErrorException is something we use internally to represent
522  // PHP errors (runtime warnings that aren't thrown or caught),
523  // don't bother putting it in the logs. Let the log message
524  // lead with "PHP Warning: " instead (see ::handleError).
525  $message = get_class( $e ) . ": $message";
526  }
527 
528  return "[{reqId}] {exception_url} $message";
529  }
530 
535  public static function getPublicLogMessage( Throwable $e ) {
536  $reqId = WebRequest::getRequestId();
537  $type = get_class( $e );
538  return '[' . $reqId . '] '
539  . gmdate( 'Y-m-d H:i:s' ) . ': '
540  . 'Fatal exception of type "' . $type . '"';
541  }
542 
555  public static function getLogContext( Throwable $e, $catcher = self::CAUGHT_BY_OTHER ) {
556  $context = [
557  'exception' => $e,
558  'exception_url' => self::getURL() ?: '[no req]',
559  // The reqId context key use the same familiar name and value as the top-level field
560  // provided by LogstashFormatter. However, formatters are configurable at run-time,
561  // and their top-level fields are logically separate from context keys and cannot be,
562  // substituted in a message, hence set explicitly here. For WMF users, these may feel,
563  // like the same thing due to Monolog V0 handling, which transmits "fields" and "context",
564  // in the same JSON object (after message formatting).
565  'reqId' => WebRequest::getRequestId(),
566  'caught_by' => $catcher
567  ];
568  if ( $e instanceof INormalizedException ) {
569  $context += $e->getMessageContext();
570  }
571  return $context;
572  }
573 
586  public static function getStructuredExceptionData(
587  Throwable $e,
588  $catcher = self::CAUGHT_BY_OTHER
589  ) {
591 
592  $data = [
593  'id' => WebRequest::getRequestId(),
594  'type' => get_class( $e ),
595  'file' => $e->getFile(),
596  'line' => $e->getLine(),
597  'message' => $e->getMessage(),
598  'code' => $e->getCode(),
599  'url' => self::getURL() ?: null,
600  'caught_by' => $catcher
601  ];
602 
603  if ( $e instanceof ErrorException &&
604  ( error_reporting() & $e->getSeverity() ) === 0
605  ) {
606  // Flag surpressed errors
607  $data['suppressed'] = true;
608  }
609 
610  if ( $wgLogExceptionBacktrace ) {
611  $data['backtrace'] = self::getRedactedTrace( $e );
612  }
613 
614  $previous = $e->getPrevious();
615  if ( $previous !== null ) {
616  $data['previous'] = self::getStructuredExceptionData( $previous, $catcher );
617  }
618 
619  return $data;
620  }
621 
676  public static function jsonSerializeException(
677  Throwable $e,
678  $pretty = false,
679  $escaping = 0,
680  $catcher = self::CAUGHT_BY_OTHER
681  ) {
682  return FormatJson::encode(
683  self::getStructuredExceptionData( $e, $catcher ),
684  $pretty,
685  $escaping
686  );
687  }
688 
700  public static function logException(
701  Throwable $e,
702  $catcher = self::CAUGHT_BY_OTHER,
703  $extraData = []
704  ) {
705  if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
706  $logger = LoggerFactory::getInstance( 'exception' );
707  $context = self::getLogContext( $e, $catcher );
708  if ( $extraData ) {
709  $context['extraData'] = $extraData;
710  }
711  $logger->error(
712  self::getLogNormalMessage( $e ),
713  $context
714  );
715 
716  $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
717  if ( $json !== false ) {
718  $logger = LoggerFactory::getInstance( 'exception-json' );
719  $logger->error( $json, [ 'private' => true ] );
720  }
721 
722  Hooks::runner()->onLogException( $e, false );
723  }
724  }
725 
734  private static function logError(
735  ErrorException $e,
736  $channel,
737  $level,
738  $catcher
739  ) {
740  // The set_error_handler callback is independent from error_reporting.
741  // Filter out unwanted errors manually (e.g. when
742  // Wikimedia\suppressWarnings is active).
743  $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
744  if ( !$suppressed ) {
745  $logger = LoggerFactory::getInstance( $channel );
746  $logger->log(
747  $level,
748  self::getLogNormalMessage( $e ),
749  self::getLogContext( $e, $catcher )
750  );
751  }
752 
753  // Include all errors in the json log (surpressed errors will be flagged)
754  $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
755  if ( $json !== false ) {
756  $logger = LoggerFactory::getInstance( "{$channel}-json" );
757  // Unlike the 'error' channel, the 'error-json' channel is unfiltered,
758  // and emits messages even if wikimedia/at-ease was used to suppress the
759  // error. To avoid clobbering Logstash dashboards with these, make sure
760  // those have their level casted to DEBUG so that they are excluded by
761  // level-based filteres automatically instead of requiring a dedicated filter
762  // for this channel. To be improved: T193472.
763  $unfilteredLevel = $suppressed ? LogLevel::DEBUG : $level;
764  $logger->log( $unfilteredLevel, $json, [ 'private' => true ] );
765  }
766 
767  Hooks::runner()->onLogException( $e, $suppressed );
768  }
769 }
MWExceptionHandler\handleFatalError
static handleFatalError()
Callback used as a registered shutdown function.
Definition: MWExceptionHandler.php:313
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:35
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:199
$wgRequest
$wgRequest
Definition: Setup.php:705
MWExceptionHandler\logError
static logError(ErrorException $e, $channel, $level, $catcher)
Log an exception that wasn't thrown but made to wrap an error.
Definition: MWExceptionHandler.php:734
MWExceptionHandler\getPublicLogMessage
static getPublicLogMessage(Throwable $e)
Definition: MWExceptionHandler.php:535
MWExceptionHandler
Handler class for MWExceptions.
Definition: MWExceptionHandler.php:32
MWExceptionHandler\redactTrace
static redactTrace(array $trace)
Redact a stacktrace generated by Throwable::getTrace(), debug_backtrace() or similar means.
Definition: MWExceptionHandler.php:449
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
MWExceptionHandler\installHandler
static installHandler()
Install handlers with PHP.
Definition: MWExceptionHandler.php:67
MWExceptionHandler\getStructuredExceptionData
static getStructuredExceptionData(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a structured representation of a Throwable.
Definition: MWExceptionHandler.php:586
Wikimedia\Rdbms\DBError
Database error base class @newable.
Definition: DBError.php:32
FormatJson\ALL_OK
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
$wgPropagateErrors
$wgPropagateErrors
If true, the MediaWiki error handler passes errors/warnings to the default error handler after loggin...
Definition: DefaultSettings.php:7430
MWExceptionHandler\logException
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Definition: MWExceptionHandler.php:700
MWExceptionHandler\report
static report(Throwable $e)
Report a throwable to the user.
Definition: MWExceptionHandler.php:98
MWExceptionHandler\getRedactedTraceAsString
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
Definition: MWExceptionHandler.php:375
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
MWException
MediaWiki exception.
Definition: MWException.php:29
MWExceptionHandler\prettyPrintTrace
static prettyPrintTrace(array $trace, $pad='')
Generate a string representation of a stacktrace.
Definition: MWExceptionHandler.php:388
MWExceptionHandler\handleException
static handleException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Exception handler which simulates the appropriate catch() handling:
Definition: MWExceptionHandler.php:200
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
MWExceptionHandler\getURL
static getURL()
If the exception occurred in the course of responding to a request, returns the requested URL.
Definition: MWExceptionHandler.php:467
MWExceptionHandler\$fatalErrorTypes
static array $fatalErrorTypes
Error types that, if unhandled, are fatal to the request.
Definition: MWExceptionHandler.php:53
MediaWiki
A helper class for throttling authentication attempts.
MWExceptionRenderer\output
static output(Throwable $e, $mode, Throwable $eNew=null)
Definition: MWExceptionRenderer.php:41
$wgLogExceptionBacktrace
$wgLogExceptionBacktrace
If true, send the exception backtrace to the error log.
Definition: DefaultSettings.php:7424
MWExceptionHandler\$reservedMemory
static string $reservedMemory
Definition: MWExceptionHandler.php:37
MWExceptionRenderer\AS_PRETTY
const AS_PRETTY
Definition: MWExceptionRenderer.php:34
Wikimedia\Rdbms\DBQueryError
@newable
Definition: DBQueryError.php:29
wfIsCLI
wfIsCLI()
Check if we are running from the commandline.
Definition: GlobalFunctions.php:1713
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MWExceptionHandler\getLogNormalMessage
static getLogNormalMessage(Throwable $e)
Get a normalised message for formatting with PSR-3 log event context.
Definition: MWExceptionHandler.php:514
$line
$line
Definition: mcc.php:119
MWExceptionHandler\getLogContext
static getLogContext(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Get a PSR-3 log event context from a Throwable.
Definition: MWExceptionHandler.php:555
MWExceptionHandler\handleUncaughtException
static handleUncaughtException(Throwable $e)
Callback to use with PHP's set_exception_handler.
Definition: MWExceptionHandler.php:169
MWExceptionHandler\rollbackPrimaryChangesAndLog
static rollbackPrimaryChangesAndLog(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Roll back any open database transactions and log the stack trace of the throwable.
Definition: MWExceptionHandler.php:126
MWExceptionHandler\rollbackMasterChangesAndLog
static rollbackMasterChangesAndLog(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Definition: MWExceptionHandler.php:155
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:333
WebRequest\getGlobalRequestURL
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.
Definition: WebRequest.php:911
MWExceptionHandler\getRedactedTrace
static getRedactedTrace(Throwable $e)
Return a copy of a throwable's backtrace as an array.
Definition: MWExceptionHandler.php:435
MWDebug\parseCallerDescription
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition: MWDebug.php:473
MWExceptionHandler\handleError
static handleError( $level, $message, $file=null, $line=null)
Handler for set_error_handler() callback notifications.
Definition: MWExceptionHandler.php:219
MWExceptionHandler\jsonSerializeException
static jsonSerializeException(Throwable $e, $pretty=false, $escaping=0, $catcher=self::CAUGHT_BY_OTHER)
Serialize a Throwable object to JSON.
Definition: MWExceptionHandler.php:676
MWExceptionHandler\getLogMessage
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
Definition: MWExceptionHandler.php:486
MWExceptionRenderer\AS_RAW
const AS_RAW
Definition: MWExceptionRenderer.php:33
$type
$type
Definition: testCompression.php:52