feat(middlewares): add EnsureApiV1AcceptsJson and SetApiV1Etag middlewares
Introduce new middlewares to enforce JSON acceptance and manage ETag headers for API responses.
This commit is contained in:
parent
8f82f86966
commit
0a1d56fc69
3 changed files with 119 additions and 0 deletions
|
|
@ -3,6 +3,8 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Presentation\Api\V1\Http\Middlewares\EnsureApiV1AcceptsJson;
|
||||
use Presentation\Api\V1\Http\Middlewares\SetApiV1Etag;
|
||||
use Presentation\Api\V1\Http\Middlewares\TransformApiV1Response;
|
||||
|
||||
return function (Middleware $middleware): void {
|
||||
|
|
@ -13,7 +15,9 @@ return function (Middleware $middleware): void {
|
|||
$middleware->api(append: []);
|
||||
|
||||
$middleware->group('api.v1', [
|
||||
EnsureApiV1AcceptsJson::class,
|
||||
'throttle:api.v1',
|
||||
SetApiV1Etag::class,
|
||||
TransformApiV1Response::class,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class EnsureApiV1AcceptsJson
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// * Bypass for OpenAPI YAML spec route
|
||||
if ($request->routeIs('api.v1.openapi')) {
|
||||
/** @var Response */
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$acceptHeader = $request->header('Accept');
|
||||
|
||||
if ($acceptHeader !== null && $acceptHeader !== '' && $acceptHeader !== '*/*') {
|
||||
$acceptableTypes = $request->getAcceptableContentTypes();
|
||||
$acceptsJson = false;
|
||||
|
||||
foreach ($acceptableTypes as $type) {
|
||||
if ($type === '*/*' || $type === 'application/*' || str_contains($type, 'json')) {
|
||||
$acceptsJson = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $acceptsJson) {
|
||||
return response()->json([
|
||||
'type' => $request->schemeAndHttpHost() . '/errors/406',
|
||||
'title' => 'Not Acceptable',
|
||||
'status' => 406,
|
||||
'detail' => 'The requested media type is not supported by this endpoint. This API only serves application/json.',
|
||||
'instance' => $request->getPathInfo(),
|
||||
], 406, ['Content-Type' => 'application/problem+json']);
|
||||
}
|
||||
}
|
||||
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
/** @var Response */
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
64
src/Presentation/Api/V1/Http/Middlewares/SetApiV1Etag.php
Normal file
64
src/Presentation/Api/V1/Http/Middlewares/SetApiV1Etag.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class SetApiV1Etag
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
/** @var Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
if (! $request->isMethodCacheable() || $response->getStatusCode() !== 200) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$etag = null;
|
||||
|
||||
if ($response instanceof BinaryFileResponse) {
|
||||
$path = $response->getFile()->getPathname();
|
||||
if (file_exists($path)) {
|
||||
$etag = '"' . hash_file('sha256', $path) . '"';
|
||||
}
|
||||
} else {
|
||||
$content = $response->getContent();
|
||||
if ($content !== false && $content !== '') {
|
||||
$etag = '"' . hash('sha256', $content) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
if ($etag === null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->headers->set('ETag', $etag);
|
||||
|
||||
$ifNoneMatch = $request->header('If-None-Match');
|
||||
if ($ifNoneMatch !== null && $this->matchesEtag($ifNoneMatch, $etag)) {
|
||||
$response->setStatusCode(304);
|
||||
$response->setContent('');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function matchesEtag(string $ifNoneMatch, string $etag): bool
|
||||
{
|
||||
$candidates = array_map('trim', explode(',', $ifNoneMatch));
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if ($candidate === '*' || $candidate === $etag || trim($candidate, '"') === trim($etag, '"')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue