Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
20 / 20 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
| Problems | |
100.00% |
20 / 20 |
|
100.00% |
3 / 3 |
14 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| process | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| httpError | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
8 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Cosray\Api; |
| 6 | |
| 7 | use Celema\Core\Exception\HttpError; |
| 8 | use Celema\Core\Exception\HttpUnauthorized; |
| 9 | use Celema\Quma\Exception\ConnectionFailed; |
| 10 | use Cosray\Exception\DatabaseUnavailable; |
| 11 | use Override; |
| 12 | use Psr\Http\Message\ResponseInterface as Response; |
| 13 | use Psr\Http\Message\ServerRequestInterface as Request; |
| 14 | use Psr\Http\Server\MiddlewareInterface as Middleware; |
| 15 | use Psr\Http\Server\RequestHandlerInterface as Handler; |
| 16 | use Psr\Log\LoggerInterface as Logger; |
| 17 | use Throwable; |
| 18 | |
| 19 | /** |
| 20 | * Turns every failure inside the API into problem details, so a site's own |
| 21 | * error renderer never changes what API clients receive. |
| 22 | */ |
| 23 | final class Problems implements Middleware |
| 24 | { |
| 25 | private const int RETRY_AFTER = 30; |
| 26 | |
| 27 | public function __construct( |
| 28 | private readonly Responses $responses, |
| 29 | private readonly Logger $logger, |
| 30 | private readonly bool $debug = false, |
| 31 | ) {} |
| 32 | |
| 33 | #[Override] |
| 34 | public function process(Request $request, Handler $handler): Response |
| 35 | { |
| 36 | try { |
| 37 | return $handler->handle($request); |
| 38 | } catch (ConnectionFailed|DatabaseUnavailable $e) { |
| 39 | $this->logger->error('API request failed: the database is unavailable', ['exception' => $e]); |
| 40 | |
| 41 | return $this->responses->problem(503, headers: ['Retry-After' => (string) self::RETRY_AFTER]); |
| 42 | } catch (HttpError $e) { |
| 43 | return $this->httpError($e); |
| 44 | } catch (Throwable $e) { |
| 45 | $this->logger->error('API request failed: {message}', ['message' => $e->getMessage(), 'exception' => $e]); |
| 46 | |
| 47 | return $this->responses->problem(500, $this->debug ? $e->getMessage() : null); |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | private function httpError(HttpError $e): Response |
| 52 | { |
| 53 | $payload = $e->payload(); |
| 54 | $detail = is_array($payload) && is_string($payload['message'] ?? null) ? $payload['message'] : null; |
| 55 | $extensions = is_array($payload) && is_array($payload['errors'] ?? null) |
| 56 | ? ['errors' => $payload['errors']] |
| 57 | : []; |
| 58 | $headers = $e instanceof HttpUnauthorized ? ['WWW-Authenticate' => 'Bearer realm="api"'] : []; |
| 59 | $status = $e->statusCode(); |
| 60 | |
| 61 | // Store reports invalid content as a bad request with the issues attached. |
| 62 | if ($status === 400 && $extensions !== []) { |
| 63 | $status = 422; |
| 64 | } |
| 65 | |
| 66 | return $this->responses->problem($status, $detail, $extensions, $headers); |
| 67 | } |
| 68 | } |