Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Problems
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
3 / 3
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 httpError
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Api;
6
7use Celema\Core\Exception\HttpError;
8use Celema\Core\Exception\HttpUnauthorized;
9use Celema\Quma\Exception\ConnectionFailed;
10use Cosray\Exception\DatabaseUnavailable;
11use Override;
12use Psr\Http\Message\ResponseInterface as Response;
13use Psr\Http\Message\ServerRequestInterface as Request;
14use Psr\Http\Server\MiddlewareInterface as Middleware;
15use Psr\Http\Server\RequestHandlerInterface as Handler;
16use Psr\Log\LoggerInterface as Logger;
17use 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 */
23final 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}