feat(api): honour header toggles for versioning

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 22:45:03 +00:00
parent ccfbe57faf
commit cebad9b77b
4 changed files with 92 additions and 9 deletions

View file

@ -37,6 +37,10 @@ class ApiSunset
/** @var Response $response */
$response = $next($request);
if (! (bool) config('api.headers.include_deprecation', true)) {
return $response;
}
$response->headers->set('Deprecation', 'true');
if ($sunsetDate !== '') {

View file

@ -105,11 +105,16 @@ class ApiVersion
/** @var Response $response */
$response = $next($request);
// Add version header to response
$response->headers->set('X-API-Version', (string) $version);
$includeVersionHeader = (bool) config('api.headers.include_version', true);
$includeDeprecationHeaders = (bool) config('api.headers.include_deprecation', true);
// Add version header to response when enabled
if ($includeVersionHeader) {
$response->headers->set('X-API-Version', (string) $version);
}
// Add deprecation headers if applicable
if (in_array($version, $deprecated, true)) {
if ($includeDeprecationHeaders && in_array($version, $deprecated, true)) {
$response->headers->set('Deprecation', 'true');
$response->headers->set('X-API-Warn', "API version {$version} is deprecated. Please upgrade to v{$current}.");
@ -217,6 +222,12 @@ class ApiVersion
*/
protected function unsupportedVersion(int $requested, array $supported, int $current): Response
{
$headers = [];
if ((bool) config('api.headers.include_version', true)) {
$headers['X-API-Version'] = (string) $current;
}
return response()->json([
'error' => 'unsupported_api_version',
'message' => "API version {$requested} is not supported.",
@ -224,9 +235,7 @@ class ApiVersion
'supported_versions' => $supported,
'current_version' => $current,
'hint' => 'Use Accept-Version header or URL prefix (e.g., /api/v1/) to specify version.',
], 400, [
'X-API-Version' => (string) $current,
]);
], 400, $headers);
}
/**
@ -234,13 +243,17 @@ class ApiVersion
*/
protected function versionTooLow(int $requested, int $required): Response
{
$headers = [];
if ((bool) config('api.headers.include_version', true)) {
$headers['X-API-Version'] = (string) $requested;
}
return response()->json([
'error' => 'api_version_too_low',
'message' => "This endpoint requires API version {$required} or higher.",
'requested_version' => $requested,
'minimum_version' => $required,
], 400, [
'X-API-Version' => (string) $requested,
]);
], 400, $headers);
}
}

View file

@ -4,9 +4,12 @@ declare(strict_types=1);
use Core\Front\Api\Middleware\ApiSunset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;
it('adds deprecation headers without a sunset date', function () {
Config::set('api.headers.include_deprecation', true);
$middleware = new ApiSunset();
$request = Request::create('/legacy-endpoint', 'GET');
@ -19,6 +22,8 @@ it('adds deprecation headers without a sunset date', function () {
});
it('adds a replacement link without a sunset date', function () {
Config::set('api.headers.include_deprecation', true);
$middleware = new ApiSunset();
$request = Request::create('/old-endpoint', 'GET');
@ -31,6 +36,8 @@ it('adds a replacement link without a sunset date', function () {
});
it('formats the sunset date and keeps the replacement link', function () {
Config::set('api.headers.include_deprecation', true);
$middleware = new ApiSunset();
$request = Request::create('/legacy-endpoint', 'GET');
@ -41,3 +48,17 @@ it('formats the sunset date and keeps the replacement link', function () {
expect($response->headers->get('Link'))->toBe('</api/v2/users>; rel="successor-version"');
expect($response->headers->get('X-API-Warn'))->toBe('This endpoint is deprecated and will be removed on 2025-06-01.');
});
it('skips deprecation headers when they are disabled in configuration', function () {
Config::set('api.headers.include_deprecation', false);
$middleware = new ApiSunset();
$request = Request::create('/legacy-endpoint', 'GET');
$response = $middleware->handle($request, fn () => new Response('OK'), '2025-06-01', '/api/v2/users');
expect($response->headers->has('Deprecation'))->toBeFalse();
expect($response->headers->has('Sunset'))->toBeFalse();
expect($response->headers->has('Link'))->toBeFalse();
expect($response->headers->has('X-API-Warn'))->toBeFalse();
});

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
use Core\Front\Api\Middleware\ApiVersion;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;
beforeEach(function () {
Config::set('api.versioning.default', 1);
Config::set('api.versioning.current', 2);
Config::set('api.versioning.supported', [1, 2]);
Config::set('api.versioning.deprecated', [1]);
Config::set('api.versioning.sunset', [
1 => '2025-06-01',
]);
Config::set('api.headers.include_version', true);
Config::set('api.headers.include_deprecation', true);
});
it('skips the api version header when it is disabled in configuration', function () {
Config::set('api.headers.include_version', false);
$middleware = new ApiVersion();
$request = Request::create('/api/users', 'GET');
$response = $middleware->handle($request, fn () => new Response('OK'));
expect($response->headers->has('X-API-Version'))->toBeFalse();
});
it('skips deprecation headers when they are disabled in configuration', function () {
Config::set('api.headers.include_deprecation', false);
$middleware = new ApiVersion();
$request = Request::create('/api/v1/users', 'GET');
$response = $middleware->handle($request, fn () => new Response('OK'));
expect($response->headers->get('X-API-Version'))->toBe('1');
expect($response->headers->has('Deprecation'))->toBeFalse();
expect($response->headers->has('Sunset'))->toBeFalse();
expect($response->headers->has('X-API-Warn'))->toBeFalse();
});