[PHP-Symfony] revamp the computation of the contentType (#21292)

recent commit 40894382fc9fe959f3beacd20cfd6421eaf840b0 already improved
that method: before that other commit it was juste impossible to query
a endpoint with a response type that was something else than
application/json or application/xml. With that commit it became possible
to query such endpoint provided that the client declare in its Accept
header that it can cope with */* (or provided that the client omitted
that header altogether).

But there were still cases badly handled. For instance if an endpoint
returns a response of type image/png and that it receives a query with
header "Accept: image/png", then it would reply with 406.

To avoid any other issue with type resolution, this commit revamps the
getOutputFormat function more thoroughly and does it by implementing
the specification available at
https://httpwg.org/specs/rfc9110.html#field.accept ), which means that
the format accepted by the client are ordered by the relative weights
specified it specified.
This commit is contained in:
Guillaume Turri 2025-05-19 04:09:16 +02:00 committed by GitHub
parent 45047b77f1
commit 6e2b4f99ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 92 additions and 48 deletions

View File

@ -180,34 +180,56 @@ class Controller extends AbstractController
*/
protected function getOutputFormat(string $accept, array $produced): ?string
{
// Figure out what the client accepts
$accept = preg_split("/[\s,]+/", $accept);
// First get the list of formats accepted by the client by weight.
// eg: text/html,*/*; q=0.7, text/*;q=0.8,text/plain;format=fixed;q=0.6 is turned to
// [
// text/html => 1, // because when no weight is present then the default value is 1. See https://httpwg.org/specs/rfc9110.html#quality.values .
// */* => 0.7,
// text/* => 0.8,
// text/plain => 0.6,
// ]
// So we can subsequently order that list by descending weight (because 1 is the most prefered value, 0.001 is the least preferred)
$weightedFormats = array();
foreach (explode(",", str_replace(' ', '', $accept)) as $accept) {
$exploded = explode(';', $accept);
// Remove q-factor weighting. E.g. "application/json;q=0.8" becomes "application/json"
$accept = array_map(function ($type) {return explode(';', $type)[0];}, $accept);
if (in_array('*/*', $accept) || in_array('application/*', $accept)) {
// Prefer JSON if the client has no preference
if (in_array('application/json', $produced)) {
return 'application/json';
// If no weight is present then the default value is 1 (see https://httpwg.org/specs/rfc9110.html#quality.values )
if (count($exploded) === 1) {
$weight = 1.0;
} else {
$lastItem = end($exploded);
if (str_starts_with($lastItem, "q=")) {
$weight = (float) str_replace("q=", "", $lastItem);
} else {
$weight = 1.0;
}
}
if (in_array('application/xml', $produced)) {
return 'application/xml';
$weightedFormats[$exploded[0]] = $weight;
}
arsort($weightedFormats);
// Now return the first produced format that matches
foreach (array_keys($weightedFormats) as $acceptedFormat) {
$acceptedFormatParts = explode('/', $acceptedFormat);
if (count($acceptedFormatParts) != 2) {
// badly formatted header sent by the client. Let's continue (instead of crashing)
continue;
}
$acceptedFormatType = $acceptedFormatParts[0];
$acceptedFormatSubtype = $acceptedFormatParts[1];
foreach ($produced as $producedFormat) {
if ($acceptedFormat === $producedFormat) {
return $producedFormat;
}
if ($acceptedFormatSubtype === '*' && $acceptedFormatType === explode("/", $producedFormat)[0]) {
return $producedFormat;
}
if ($acceptedFormat === "*/*") {
return $producedFormat;
}
}
}
if (in_array('application/json', $accept) && in_array('application/json', $produced)) {
return 'application/json';
}
if (in_array('application/xml', $accept) && in_array('application/xml', $produced)) {
return 'application/xml';
}
if (in_array('*/*', $accept)) {
return $produced[0];
}
// If we reach this point, we don't have a common ground between server and client
return null;
}

View File

@ -190,34 +190,56 @@ class Controller extends AbstractController
*/
protected function getOutputFormat(string $accept, array $produced): ?string
{
// Figure out what the client accepts
$accept = preg_split("/[\s,]+/", $accept);
// First get the list of formats accepted by the client by weight.
// eg: text/html,*/*; q=0.7, text/*;q=0.8,text/plain;format=fixed;q=0.6 is turned to
// [
// text/html => 1, // because when no weight is present then the default value is 1. See https://httpwg.org/specs/rfc9110.html#quality.values .
// */* => 0.7,
// text/* => 0.8,
// text/plain => 0.6,
// ]
// So we can subsequently order that list by descending weight (because 1 is the most prefered value, 0.001 is the least preferred)
$weightedFormats = array();
foreach (explode(",", str_replace(' ', '', $accept)) as $accept) {
$exploded = explode(';', $accept);
// Remove q-factor weighting. E.g. "application/json;q=0.8" becomes "application/json"
$accept = array_map(function ($type) {return explode(';', $type)[0];}, $accept);
if (in_array('*/*', $accept) || in_array('application/*', $accept)) {
// Prefer JSON if the client has no preference
if (in_array('application/json', $produced)) {
return 'application/json';
// If no weight is present then the default value is 1 (see https://httpwg.org/specs/rfc9110.html#quality.values )
if (count($exploded) === 1) {
$weight = 1.0;
} else {
$lastItem = end($exploded);
if (str_starts_with($lastItem, "q=")) {
$weight = (float) str_replace("q=", "", $lastItem);
} else {
$weight = 1.0;
}
}
if (in_array('application/xml', $produced)) {
return 'application/xml';
$weightedFormats[$exploded[0]] = $weight;
}
arsort($weightedFormats);
// Now return the first produced format that matches
foreach (array_keys($weightedFormats) as $acceptedFormat) {
$acceptedFormatParts = explode('/', $acceptedFormat);
if (count($acceptedFormatParts) != 2) {
// badly formatted header sent by the client. Let's continue (instead of crashing)
continue;
}
$acceptedFormatType = $acceptedFormatParts[0];
$acceptedFormatSubtype = $acceptedFormatParts[1];
foreach ($produced as $producedFormat) {
if ($acceptedFormat === $producedFormat) {
return $producedFormat;
}
if ($acceptedFormatSubtype === '*' && $acceptedFormatType === explode("/", $producedFormat)[0]) {
return $producedFormat;
}
if ($acceptedFormat === "*/*") {
return $producedFormat;
}
}
}
if (in_array('application/json', $accept) && in_array('application/json', $produced)) {
return 'application/json';
}
if (in_array('application/xml', $accept) && in_array('application/xml', $produced)) {
return 'application/xml';
}
if (in_array('*/*', $accept)) {
return $produced[0];
}
// If we reach this point, we don't have a common ground between server and client
return null;
}