Skip to content

Commit

Permalink
Make reporting of client-safe errors configurable (#2647)
Browse files Browse the repository at this point in the history
  • Loading branch information
remipelhate authored Jan 16, 2025
1 parent 2f0cd99 commit cc4b1d1
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 0 deletions.
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;
}
}

0 comments on commit cc4b1d1

Please sign in to comment.