-
-
Notifications
You must be signed in to change notification settings - Fork 466
Expand file tree
/
Copy pathErrorHandler.php
More file actions
498 lines (417 loc) · 18.2 KB
/
ErrorHandler.php
File metadata and controls
498 lines (417 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
<?php
declare(strict_types=1);
namespace Sentry;
use Sentry\Exception\FatalErrorException;
use Sentry\Exception\SilencedErrorException;
/**
* This class implements a simple error handler that catches all configured
* error types and relays them to all configured listeners. Registering this
* error handler more than once is not supported and will lead to nasty
* problems. The code is based on the Symfony ErrorHandler component.
*
* @psalm-import-type StacktraceFrame from FrameBuilder
*/
final class ErrorHandler
{
/**
* The default amount of bytes of memory to reserve for the fatal error handler.
*
* @internal
*/
public const DEFAULT_RESERVED_MEMORY_SIZE = 16 * 1024; // 16 KiB
/**
* The regular expression used to match the message of an out of memory error.
*
* Regex inspired by https://github.com/php/php-src/blob/524b13460752fba908f88e3c4428b91fa66c083a/Zend/tests/new_oom.phpt#L15
*/
private const OOM_MESSAGE_MATCHER = '/^Allowed memory size of (?<memory_limit>\d+) bytes exhausted[^\r\n]* \(tried to allocate \d+ bytes\)/';
/**
* The fatal error types that cannot be silenced using the @ operator in PHP 8+.
*/
private const PHP8_UNSILENCEABLE_FATAL_ERRORS = \E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR;
/**
* @var self|null The current registered handler (this class is a singleton)
*/
private static $handlerInstance;
/**
* @var callable[] List of listeners that will act on each captured error
*
* @psalm-var (callable(\ErrorException): void)[]
*/
private $errorListeners = [];
/**
* @var callable[] List of listeners that will act of each captured fatal error
*
* @psalm-var (callable(FatalErrorException): void)[]
*/
private $fatalErrorListeners = [];
/**
* @var callable[] List of listeners that will act on each captured exception
*
* @psalm-var (callable(\Throwable): void)[]
*/
private $exceptionListeners = [];
/**
* @var \ReflectionProperty A reflection cached instance that points to the
* trace property of the exception objects
*/
private $exceptionReflection;
/**
* @var callable|null The previous error handler, if any
*/
private $previousErrorHandler;
/**
* @var callable|null The previous exception handler, if any
*
* @psalm-var null|callable(\Throwable): void
*/
private $previousExceptionHandler;
/**
* @var bool Whether the error handler has been registered
*/
private $isErrorHandlerRegistered = false;
/**
* @var bool Whether the exception handler has been registered
*/
private $isExceptionHandlerRegistered = false;
/**
* @var bool Whether the fatal error handler has been registered
*/
private $isFatalErrorHandlerRegistered = false;
/**
* @var int|null the amount of bytes of memory to increase the memory limit by when we are capturing a out of memory error, set to null to not increase the memory limit
*/
private $memoryLimitIncreaseOnOutOfMemoryErrorValue = 5 * 1024 * 1024; // 5 MiB
/**
* @var Options|null The SDK options
*/
private $options;
/**
* @var bool Whether the memory limit has been increased
*/
private static $didIncreaseMemoryLimit = false;
/**
* @var string|null A portion of pre-allocated memory data that will be reclaimed in case a fatal error occurs to handle it
*
* @phpstan-ignore-next-line This property is used to reserve memory for the fatal error handler and is thus never read
*/
private static $reservedMemory;
/**
* @var bool Whether the fatal error handler should be disabled
*/
private static $disableFatalErrorHandler = false;
/**
* @var string[] List of error levels and their description
*/
private const ERROR_LEVELS_DESCRIPTION = [
\E_DEPRECATED => 'Deprecated',
\E_USER_DEPRECATED => 'User Deprecated',
\E_NOTICE => 'Notice',
\E_USER_NOTICE => 'User Notice',
\E_STRICT => 'Runtime Notice',
\E_WARNING => 'Warning',
\E_USER_WARNING => 'User Warning',
\E_COMPILE_WARNING => 'Compile Warning',
\E_CORE_WARNING => 'Core Warning',
\E_USER_ERROR => 'User Error',
\E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
\E_COMPILE_ERROR => 'Compile Error',
\E_PARSE => 'Parse Error',
\E_ERROR => 'Error',
\E_CORE_ERROR => 'Core Error',
];
/**
* Constructor.
*
* @throws \ReflectionException If hooking into the \Exception class to
* make the `trace` property accessible fails
*/
private function __construct()
{
$this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace');
$this->exceptionReflection->setAccessible(true);
}
/**
* Registers the error handler once and returns its instance.
*/
public static function registerOnceErrorHandler(?Options $options = null): self
{
if (self::$handlerInstance === null) {
self::$handlerInstance = new self();
}
self::$handlerInstance->options = $options;
if (self::$handlerInstance->isErrorHandlerRegistered) {
return self::$handlerInstance;
}
$errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance, 'handleError']);
self::$handlerInstance->isErrorHandlerRegistered = true;
self::$handlerInstance->previousErrorHandler = set_error_handler($errorHandlerCallback);
if (self::$handlerInstance->previousErrorHandler === null) {
restore_error_handler();
// Specifying the error types caught by the error handler with the
// first call to the set_error_handler method would cause the PHP
// bug https://bugs.php.net/63206 if the handler is not the first
// one in the chain of handlers
set_error_handler($errorHandlerCallback, \E_ALL);
}
return self::$handlerInstance;
}
/**
* Registers the fatal error handler and reserves a certain amount of memory
* that will be reclaimed to handle the errors (to prevent out of memory
* issues while handling them) and returns its instance.
*
* @param int $reservedMemorySize The amount of memory to reserve for the fatal
* error handler expressed in bytes
*/
public static function registerOnceFatalErrorHandler(int $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE): self
{
if ($reservedMemorySize <= 0) {
throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.');
}
if (self::$handlerInstance === null) {
self::$handlerInstance = new self();
}
if (self::$handlerInstance->isFatalErrorHandlerRegistered) {
return self::$handlerInstance;
}
self::$handlerInstance->isFatalErrorHandlerRegistered = true;
self::$reservedMemory = str_repeat('x', $reservedMemorySize);
register_shutdown_function(\Closure::fromCallable([self::$handlerInstance, 'handleFatalError']));
return self::$handlerInstance;
}
/**
* Registers the exception handler, effectively replacing the current one
* and returns its instance. The previous one will be saved anyway and
* called when appropriate.
*/
public static function registerOnceExceptionHandler(): self
{
if (self::$handlerInstance === null) {
self::$handlerInstance = new self();
}
if (self::$handlerInstance->isExceptionHandlerRegistered) {
return self::$handlerInstance;
}
self::$handlerInstance->isExceptionHandlerRegistered = true;
self::$handlerInstance->previousExceptionHandler = set_exception_handler(\Closure::fromCallable([self::$handlerInstance, 'handleException']));
return self::$handlerInstance;
}
/**
* Adds a listener to the current error handler that will be called every
* time an error is captured.
*
* @param callable $listener A callable that will act as a listener
* and that must accept a single argument
* of type \ErrorException
*
* @psalm-param callable(\ErrorException): void $listener
*/
public function addErrorHandlerListener(callable $listener): void
{
$this->errorListeners[] = $listener;
}
/**
* Adds a listener to the current error handler that will be called every
* time a fatal error handler is captured.
*
* @param callable $listener A callable that will act as a listener
* and that must accept a single argument
* of type \Sentry\Exception\FatalErrorException
*
* @psalm-param callable(FatalErrorException): void $listener
*/
public function addFatalErrorHandlerListener(callable $listener): void
{
$this->fatalErrorListeners[] = $listener;
}
/**
* Adds a listener to the current error handler that will be called every
* time an exception is captured.
*
* @param callable $listener A callable that will act as a listener
* and that must accept a single argument
* of type \Throwable
*
* @psalm-param callable(\Throwable): void $listener
*/
public function addExceptionHandlerListener(callable $listener): void
{
$this->exceptionListeners[] = $listener;
}
/**
* Sets the amount of memory to increase the memory limit by when we are capturing a out of memory error.
*
* @param int|null $valueInBytes the number of bytes to increase the memory limit by, or null to not increase the memory limit
*/
public function setMemoryLimitIncreaseOnOutOfMemoryErrorInBytes(?int $valueInBytes): void
{
if ($valueInBytes !== null && $valueInBytes <= 0) {
throw new \InvalidArgumentException('The $valueInBytes argument must be greater than 0 or null.');
}
$this->memoryLimitIncreaseOnOutOfMemoryErrorValue = $valueInBytes;
}
/**
* Handles errors by capturing them through the client according to the
* configured bit field.
*
* @param int $level The level of the error raised, represented by
* one of the E_* constants
* @param string $message The error message
* @param string $file The filename the error was raised in
* @param int $line The line number the error was raised at
* @param array<string, mixed>|null $errcontext The error context (deprecated since PHP 7.2)
*
* @return bool If the function returns `false` then the PHP native error
* handler will be called
*
* @throws \Throwable
*/
private function handleError(int $level, string $message, string $file, int $line, ?array $errcontext = []): bool
{
$isSilencedError = error_reporting() === 0;
if (\PHP_MAJOR_VERSION >= 8) {
// Starting from PHP8, when a silenced error occurs the `error_reporting()`
// function will return a bitmask of fatal errors that are unsilenceable.
// If by subtracting from this value those errors the result is 0, we can
// conclude that the error was silenced.
$isSilencedError = 0 === (error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS);
// However, starting from PHP8 some fatal errors are unsilenceable,
// so we have to check for them to avoid reporting any of them as
// silenced instead
if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS & $level)) {
$isSilencedError = false;
}
}
if ($this->shouldHandleError($level, $isSilencedError)) {
if ($isSilencedError) {
$errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
} else {
$errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line);
}
$backtrace = $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine());
$this->exceptionReflection->setValue($errorAsException, $backtrace);
$this->invokeListeners($this->errorListeners, $errorAsException);
}
if ($this->previousErrorHandler !== null) {
return false !== ($this->previousErrorHandler)($level, $message, $file, $line, $errcontext);
}
return false;
}
private function shouldHandleError(int $level, bool $silenced): bool
{
// If we were not given any options, we should handle all errors
if ($this->options === null) {
return true;
}
if ($silenced) {
return $this->options->shouldCaptureSilencedErrors();
}
return ($this->options->getErrorTypes() & $level) !== 0;
}
/**
* Tries to handle a fatal error if any and relay them to the listeners.
* It only tries to do this if we still have some reserved memory at
* disposal. This method is used as callback of a shutdown function.
*/
private function handleFatalError(): void
{
if (self::$disableFatalErrorHandler) {
return;
}
// Free the reserved memory that allows us to potentially handle OOM errors
self::$reservedMemory = null;
$error = error_get_last();
if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) {
// If we did not do so already and we are allowed to increase the memory limit, we do so when we detect an OOM error
if (self::$didIncreaseMemoryLimit === false
&& $this->memoryLimitIncreaseOnOutOfMemoryErrorValue !== null
&& preg_match(self::OOM_MESSAGE_MATCHER, $error['message'], $matches) === 1
) {
$currentMemoryLimit = (int) $matches['memory_limit'];
ini_set('memory_limit', (string) ($currentMemoryLimit + $this->memoryLimitIncreaseOnOutOfMemoryErrorValue));
self::$didIncreaseMemoryLimit = true;
}
$errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line']);
$this->exceptionReflection->setValue($errorAsException, []);
$this->invokeListeners($this->fatalErrorListeners, $errorAsException);
}
}
/**
* Handles the given exception by passing it to all the listeners,
* then forwarding it to another handler.
*
* @param \Throwable $exception The exception to handle
*
* @throws \Throwable
*/
private function handleException(\Throwable $exception): void
{
$this->invokeListeners($this->exceptionListeners, $exception);
$previousExceptionHandlerException = $exception;
// Unset the previous exception handler to prevent infinite loop in case
// we need to handle an exception thrown from it
$previousExceptionHandler = $this->previousExceptionHandler;
$this->previousExceptionHandler = null;
try {
if ($previousExceptionHandler !== null) {
$previousExceptionHandler($exception);
return;
}
} catch (\Throwable $previousExceptionHandlerException) {
// This `catch` statement is here to forcefully override the
// $previousExceptionHandlerException variable with the exception
// we just caught
}
// If the instance of the exception we're handling is the same as the one
// caught from the previous exception handler then we give it back to the
// native PHP handler to prevent an infinite loop
if ($exception === $previousExceptionHandlerException) {
// Disable the fatal error handler or the error will be reported twice
self::$disableFatalErrorHandler = true;
throw $exception;
}
$this->handleException($previousExceptionHandlerException);
}
/**
* Cleans and returns the backtrace without the first frames that belong to
* this error handler.
*
* @param array<int, array<string, mixed>> $backtrace The backtrace to clear
* @param string $file The filename the backtrace was raised in
* @param int $line The line number the backtrace was raised at
*
* @return array<int, mixed>
*
* @psalm-param list<StacktraceFrame> $backtrace
*/
private function cleanBacktraceFromErrorHandlerFrames(array $backtrace, string $file, int $line): array
{
$cleanedBacktrace = $backtrace;
$index = 0;
while ($index < \count($backtrace)) {
if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) {
$cleanedBacktrace = \array_slice($cleanedBacktrace, 1 + $index);
break;
}
++$index;
}
return $cleanedBacktrace;
}
/**
* Invokes all the listeners and pass the exception to all of them.
*
* @param callable[] $listeners The array of listeners to be called
* @param \Throwable $throwable The exception to be passed onto listeners
*/
private function invokeListeners(array $listeners, \Throwable $throwable): void
{
foreach ($listeners as $listener) {
try {
$listener($throwable);
} catch (\Throwable $exception) {
// Do nothing as this should be as transparent as possible
}
}
}
}