Skip to content

Commit

Permalink
Release v2.22.2
Browse files Browse the repository at this point in the history
  • Loading branch information
imjoehaines authored Sep 6, 2021
2 parents b18f6c8 + a50b188 commit cc5ec4e
Show file tree
Hide file tree
Showing 6 changed files with 582 additions and 59 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

## 2.22.2 (2021-09-06)

### Bug Fixes

* Fix events in Lumen always being handled
[#452](https://github.com/bugsnag/bugsnag-laravel/pull/452)

## 2.22.1 (2021-04-29)

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion src/BugsnagServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BugsnagServiceProvider extends ServiceProvider
*
* @var string
*/
const VERSION = '2.22.1';
const VERSION = '2.22.2';

/**
* Boot the service provider.
Expand Down
186 changes: 186 additions & 0 deletions src/Internal/BacktraceProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

namespace Bugsnag\BugsnagLaravel\Internal;

/**
* The criteria for an error to be unhandled in a Laravel or Lumen app is as
* follows.
*
* 1. All unhandled exceptions must pass through one of the `HANDLER_CLASSES`
* report method
* 2. Unhandled exceptions will have had a caller from inside a vendor namespace
* or the App exception handler
* 3. The above exception handler must have originally been called from
* within a vendor namespace
*/
final class BacktraceProcessor
{
/**
* Searching for a frame where the framework's error handler is called.
*/
const STATE_FRAMEWORK_HANDLER = 1;

/**
* Searching for a frame where the framework's error handler was called by
* the framework itself, or the user's exception handler.
*/
const STATE_HANDLER_CALLER = 2;

/**
* Deciding if the frame was unhandled.
*/
const STATE_IS_UNHANDLED = 3;

/**
* A state to signal that we're done processing frames and know if the error
* was unhandled.
*/
const STATE_DONE = 4;

/**
* Laravel and Lumen use different exception handlers which live in different
* namespaces.
*/
const LARAVEL_HANDLER_CLASS = \Illuminate\Foundation\Exceptions\Handler::class;
const LUMEN_HANDLER_CLASS = \Laravel\Lumen\Exceptions\Handler::class;

/**
* The method used by the x_HANDLER_CLASS to report errors.
*/
const HANDLER_METHOD = 'report';

/**
* Laravel uses the "Exception" namespace, Lumen uses the plural "Exceptions"
* for the default app exception handler.
*/
const LARAVEL_APP_EXCEPTION_HANDLER = \App\Exception\Handler::class;
const LUMEN_APP_EXCEPTION_HANDLER = \App\Exceptions\Handler::class;

/**
* Namespaces used by Laravel and Lumen so we can determine if code was
* called by the user's app or the framework itself.
*
* Note this is not a mistake - Lumen uses the 'Laravel' namespace but
* Laravel itself does not
*/
const LARAVEL_VENDOR_NAMESPACE = 'Illuminate\\';
const LUMEN_VENDOR_NAMESPACE = 'Laravel\\';

/**
* The current state; one of the self::STATE_ constants.
*
* @var int
*/
private $state = self::STATE_FRAMEWORK_HANDLER;

/**
* This flag will be set to 'true' if we determine the error was unhandled.
*
* @var bool
*/
private $unhandled = false;

/**
* A backtrace matching the format of PHP's 'debug_backtrace'.
*
* @var array
*/
private $backtrace;

/**
* @param array $backtrace
*/
public function __construct(array $backtrace)
{
$this->backtrace = $backtrace;
}

/**
* Determine if the backtrace was from an unhandled error.
*
* @return bool
*/
public function isUnhandled()
{
foreach ($this->backtrace as $frame) {
$this->processFrame($frame);

// stop iterating early if we know we're done
if ($this->state === self::STATE_DONE) {
break;
}
}

return $this->unhandled;
}

/**
* @param array $frame
*
* @return void
*/
private function processFrame(array $frame)
{
if (!isset($frame['class'])) {
return;
}

$class = $frame['class'];

switch ($this->state) {
case self::STATE_FRAMEWORK_HANDLER:
// if this class is a framework exception handler and the function
// matches self::HANDLER_METHOD, we can move on to searching for
// the caller
if (($class === self::LARAVEL_HANDLER_CLASS || $class === self::LUMEN_HANDLER_CLASS)
&& isset($frame['function'])
&& $frame['function'] === self::HANDLER_METHOD
) {
$this->state = self::STATE_HANDLER_CALLER;
}

break;

case self::STATE_HANDLER_CALLER:
// if this is an app exception handler or a framework class, we
// can move on to determine if this was unhandled or not
if ($class === self::LARAVEL_APP_EXCEPTION_HANDLER
|| $class === self::LUMEN_APP_EXCEPTION_HANDLER
|| $this->isVendor($class)
) {
$this->state = self::STATE_IS_UNHANDLED;
}

break;

case self::STATE_IS_UNHANDLED:
// we are only interested in running this once so move immediately
// into the "done" state. This ensures we only check the frame
// immediately before the caller of the exception handler
$this->state = self::STATE_DONE;

// if this class is internal to the framework then the exception
// was unhandled
if ($this->isVendor($class)) {
$this->unhandled = true;
}

break;
}
}

/**
* Does the given class belong to a vendor namespace?
*
* @see self::VENDOR_NAMESPACES
*
* @param string $class
*
* @return bool
*/
private function isVendor($class)
{
return substr($class, 0, strlen(self::LARAVEL_VENDOR_NAMESPACE)) === self::LARAVEL_VENDOR_NAMESPACE
|| substr($class, 0, strlen(self::LUMEN_VENDOR_NAMESPACE)) === self::LUMEN_VENDOR_NAMESPACE;
}
}
68 changes: 10 additions & 58 deletions src/Middleware/UnhandledState.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,11 @@

namespace Bugsnag\BugsnagLaravel\Middleware;

use Bugsnag\BugsnagLaravel\Internal\BacktraceProcessor;
use Bugsnag\Report;

class UnhandledState
{
/**
* Unhandled state middleware implementation details.
*
* This middleware functions on the basis of three things:
* 1. All unhandled exceptions must pass through the `HANDLER_CLASS` report
* method
* 2. Unhandled exceptions will have had a caller from inside the Illuminate
* namespace or App exception handler
* 3. The above exception handler must have originally been called from
* within the Illuminate namespace
*
* This middleware calls the inbuilt PHP backtrace, and traverses each frame
* to determine if the above conditions are met. If they are, the report is
* marked as unhandled.
*/
const HANDLER_CLASS = 'Illuminate\\Foundation\\Exceptions\\Handler';
const HANDLER_METHOD = 'report';
const ILLUMINATE_NAMESPACE = 'Illuminate';
const APP_EXCEPTION_HANDLER = 'App\\Exception\\Handler';

/**
* Execute the unhandled state middleware.
*
Expand All @@ -37,50 +18,21 @@ class UnhandledState
public function __invoke(Report $report, callable $next)
{
$stackFrames = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$reportFrame = false;
$callerFrame = false;
$unhandled = false;
if (is_null($stackFrames)) {
return;
}
foreach ($stackFrames as $frame) {
if (!array_key_exists('class', $frame)) {
continue;
}
$class = $frame['class'];
if (!$reportFrame) {
if (!array_key_exists('function', $frame)) {
continue;
} elseif (($class === $this::HANDLER_CLASS) && ($frame['function'] === $this::HANDLER_METHOD)) {
$reportFrame = true;
}
} elseif (!$callerFrame) {
$startsWithIlluminate = substr($class, 0, strlen($this::ILLUMINATE_NAMESPACE)) === $this::ILLUMINATE_NAMESPACE;
if ($startsWithIlluminate || ($class == $this::APP_EXCEPTION_HANDLER)) {
$callerFrame = true;
}
} elseif (!$unhandled) {
$startsWithIlluminate = substr($class, 0, strlen($this::ILLUMINATE_NAMESPACE)) === $this::ILLUMINATE_NAMESPACE;
if ($startsWithIlluminate) {
$unhandled = true;
}
break;
}

if (!is_array($stackFrames)) {
$stackFrames = [];
}
if ($unhandled) {

$backtraceProcessor = new BacktraceProcessor($stackFrames);

if ($backtraceProcessor->isUnhandled()) {
$report->setUnhandled(true);
$report->setSeverityReason([
'type' => 'unhandledExceptionMiddleware',
'attributes' => [
'framework' => 'Laravel',
],
'attributes' => ['framework' => 'Laravel'],
]);
}
$next($report);
}

protected function stringStartsWith($haystack, $needle)
{
return substr($haystack, 0, strlen($needle)) === $needle;
$next($report);
}
}
Loading

0 comments on commit cc5ec4e

Please sign in to comment.