Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make reporting of client-safe errors configurable #2647

Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Added

- Make reporting of client-safe errors configurable https://github.com/nuwave/lighthouse/issues/2647

## v6.48.0

### Added
Expand Down
15 changes: 15 additions & 0 deletions docs/master/digging-deeper/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ Client-safe errors are assumed to be something that:

Thus, they are typically not actionable for server developers.

However, as Laravel allows to define a [minimum log level](https://laravel.com/docs/errors#exception-log-levels)
at which each individual log channel is triggered, you can choose to report client-safe errors by replacing
`Nuwave\Lighthouse\Execution\ReportingErrorHandler` with `Nuwave\Lighthouse\Execution\AlwaysReportingErrorHandler`
in the `lighthouse.php` config:

```diff
'error_handlers' => [
- Nuwave\Lighthouse\Execution\ReportingErrorHandler::class,
+ Nuwave\Lighthouse\Execution\AlwaysReportingErrorHandler::class,
],
```

When using `Nuwave\Lighthouse\Execution\AlwaysReportingErrorHandler`, client-safe exceptions will be passed to the
default Laravel exception handler, allowing you to configure appropriate error reporting outside of Lighthouse.

## Additional Error Information

The interface [`GraphQL\Error\ProvidesExtensions`](https://github.com/webonyx/graphql-php/blob/master/src/Error/ProvidesExtensions.php)
Expand Down
26 changes: 26 additions & 0 deletions src/Execution/AlwaysReportingErrorHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Execution;

use GraphQL\Error\Error;
use Illuminate\Contracts\Debug\ExceptionHandler;

class AlwaysReportingErrorHandler implements ErrorHandler
{
public function __construct(
protected ExceptionHandler $exceptionHandler,
) {}

public function __invoke(?Error $error, \Closure $next): ?array
{
if ($error === null) {
return $next(null);
}

$this->exceptionHandler->report(
$error->getPrevious() ?? $error,
);

return $next($error);
}
}
40 changes: 40 additions & 0 deletions tests/FakeExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace Tests;

use Illuminate\Contracts\Debug\ExceptionHandler;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Response;

final class FakeExceptionHandler implements ExceptionHandler
{
/** @var array<\Throwable> */
private array $reported = [];

public function report(\Throwable $e): void
{
$this->reported[] = $e;
}

public function shouldReport(\Throwable $e): bool
{
return true;
}

public function assertNothingReported(): void
{
Assert::assertEmpty($this->reported);
}

public function assertReported(\Throwable $e): void
{
Assert::assertContainsEquals($e, $this->reported);
}

public function render($request, \Throwable $e): Response
{
throw $e;
}

public function renderForConsole($output, \Throwable $e) {}
}
80 changes: 80 additions & 0 deletions tests/Integration/Execution/AlwaysReportingErrorHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);

namespace Tests\Integration\Execution;

use GraphQL\Error\Error;
use Nuwave\Lighthouse\Execution\AlwaysReportingErrorHandler;
use Tests\FakeExceptionHandler;
use Tests\TestCase;
use Tests\Utils\Exceptions\ClientAwareException;

final class AlwaysReportingErrorHandlerTest extends TestCase
{
private FakeExceptionHandler $handler;

/** @before */
public function fakeExceptionHandling(): void
{
$this->afterApplicationCreated(function (): void {
$this->withoutExceptionHandling();
$this->handler = new FakeExceptionHandler();
});
$this->beforeApplicationDestroyed(function (): void {
unset($this->handler);
});
}

public function testHandlingWhenThereIsNoError(): void
{
$next = fn (?Error $error): array => match ($error) {
null => ['error' => 'No error to report'],
default => throw new \LogicException('Unexpected error: ' . $error::class),
};

$result = (new AlwaysReportingErrorHandler($this->handler))(null, $next);

$this->assertSame(['error' => 'No error to report'], $result);
$this->handler->assertNothingReported();
}

/** @return iterable<array{\Exception}> */
public static function nonClientSafeErrors(): iterable
{
yield 'Previous error is not client aware' => [new \Exception('Not client aware')];
yield 'Previous error is not client safe' => [ClientAwareException::notClientSafe()];
}

/** @dataProvider nonClientSafeErrors */
public function testNonClientSafeErrors(\Exception $previousError): void
{
$error = new Error(previous: $previousError);
$next = fn (Error $error): array => \compact('error');

$result = (new AlwaysReportingErrorHandler($this->handler))($error, $next);

$this->assertSame(\compact('error'), $result);
$this->handler->assertReported($previousError);
}

/** @return iterable<array{\Exception|null}> */
public static function clientSafeErrors(): iterable
{
yield 'No previous error' => [null];
yield 'Previous error is client safe' => [ClientAwareException::clientSafe()];
}

/** @dataProvider clientSafeErrors */
public function testClientSafeErrors(?\Exception $previousError): void
{
$error = new Error(previous: $previousError);
$next = fn (Error $error): array => \compact('error');

$result = (new AlwaysReportingErrorHandler($this->handler))($error, $next);

$this->assertSame(\compact('error'), $result);
$this->handler->assertReported(match ($previousError) {
null => $error,
default => $previousError,
});
}
}
77 changes: 77 additions & 0 deletions tests/Integration/Execution/ReportingErrorHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php declare(strict_types=1);

namespace Tests\Integration\Execution;

use GraphQL\Error\Error;
use Nuwave\Lighthouse\Execution\ReportingErrorHandler;
use Tests\FakeExceptionHandler;
use Tests\TestCase;
use Tests\Utils\Exceptions\ClientAwareException;

final class ReportingErrorHandlerTest extends TestCase
{
private FakeExceptionHandler $handler;

/** @before */
public function fakeExceptionHandling(): void
{
$this->afterApplicationCreated(function (): void {
$this->withoutExceptionHandling();
$this->handler = new FakeExceptionHandler();
});
$this->beforeApplicationDestroyed(function (): void {
unset($this->handler);
});
}

public function testHandlingWhenThereIsNoError(): void
{
$next = fn (?Error $error): array => match ($error) {
null => ['error' => 'No error to report'],
default => throw new \LogicException('Unexpected error: ' . $error::class),
};

$result = (new ReportingErrorHandler($this->handler))(null, $next);

$this->assertSame(['error' => 'No error to report'], $result);
$this->handler->assertNothingReported();
}

/** @return iterable<array{\Exception}> */
public static function nonClientSafe(): iterable
{
yield 'Previous error is not client aware' => [new \Exception('Not client aware')];
yield 'Previous error is not client safe' => [ClientAwareException::notClientSafe()];
}

/** @dataProvider nonClientSafe */
public function testNonClientSafeErrors(\Exception $previousError): void
{
$error = new Error(previous: $previousError);
$next = fn (Error $error): array => \compact('error');

$result = (new ReportingErrorHandler($this->handler))($error, $next);

$this->assertSame(\compact('error'), $result);
$this->handler->assertReported($previousError);
}

/** @return iterable<array{\Exception|null}> */
public static function clientSafeErrors(): iterable
{
yield 'No previous error' => [null];
yield 'Previous error is client safe' => [ClientAwareException::clientSafe()];
}

/** @dataProvider clientSafeErrors */
public function testClientSafeErrors(?\Exception $previousError): void
{
$error = new Error(previous: $previousError);
$next = fn (Error $error): array => \compact('error');

$result = (new ReportingErrorHandler($this->handler))($error, $next);

$this->assertSame(\compact('error'), $result);
$this->handler->assertNothingReported();
}
}
31 changes: 31 additions & 0 deletions tests/Utils/Exceptions/ClientAwareException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tests\Utils\Exceptions;

use GraphQL\Error\ClientAware;

final class ClientAwareException extends \Exception implements ClientAware
{
private function __construct(
private bool $clientSafe,
) {
parent::__construct('Client Aware Error');
}

public static function clientSafe(): self
{
return new self(true);
}

public static function notClientSafe(): self
{
return new self(false);
}

public function isClientSafe(): bool
{
return $this->clientSafe;
}
}
Loading