feat(api): harden version header parsing

Handle Accept-Version parameters and comma-separated Accept values when extracting API versions.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:43:14 +00:00
parent 812400f303
commit c21c3409d7
2 changed files with 56 additions and 4 deletions

View file

@ -188,7 +188,9 @@ class ApiVersion
return null;
}
// Strip 'v' prefix if present
// Strip 'v' prefix and any optional parameters if present.
$header = trim($header);
$header = explode(';', $header, 2)[0];
$version = ltrim($header, 'vV');
if (is_numeric($version)) {
@ -207,9 +209,14 @@ class ApiVersion
{
$accept = $request->header('Accept', '');
// Match vendor media type: application/vnd.{name}.v{n}+json
if (preg_match('#application/vnd\.[^.]+\.v(\d+)\+#', $accept, $matches)) {
return (int) $matches[1];
foreach (preg_split('/\s*,\s*/', $accept, -1, PREG_SPLIT_NO_EMPTY) ?: [] as $mediaType) {
// Strip media-type parameters before matching the vendor suffix.
$mediaType = explode(';', trim($mediaType), 2)[0];
// Match vendor media type: application/vnd.{name}.v{n}+json
if (preg_match('#^application/vnd\.[^.]+\.v(\d+)\+#i', $mediaType, $matches)) {
return (int) $matches[1];
}
}
return null;

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', []);
Config::set('api.versioning.sunset', []);
Config::set('api.headers.include_version', true);
Config::set('api.headers.include_deprecation', true);
});
it('resolves the api version from an accept-version header with parameters', function () {
$middleware = new ApiVersion();
$request = Request::create('/api/users', 'GET');
$request->headers->set('Accept-Version', 'v2; q=1.0');
$response = $middleware->handle($request, fn () => new Response('OK'));
expect($response->headers->get('X-API-Version'))->toBe('2');
expect($request->attributes->get('api_version'))->toBe(2);
expect($request->attributes->get('api_version_string'))->toBe('v2');
});
it('resolves the api version from a vendor accept header inside a list', function () {
$middleware = new ApiVersion();
$request = Request::create('/api/users', 'GET');
$request->headers->set(
'Accept',
'text/html;q=0.8, application/json, application/vnd.hosthub.v2+json; charset=utf-8'
);
$response = $middleware->handle($request, fn () => new Response('OK'));
expect($response->headers->get('X-API-Version'))->toBe('2');
expect($request->attributes->get('api_version'))->toBe(2);
expect($request->attributes->get('api_version_string'))->toBe('v2');
});