Skip to content

Commit 6e2b4f9

Browse files
authored
[PHP-Symfony] revamp the computation of the contentType (#21292)
recent commit 4089438 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.
1 parent 45047b7 commit 6e2b4f9

File tree

2 files changed

+92
-48
lines changed

2 files changed

+92
-48
lines changed

modules/openapi-generator/src/main/resources/php-symfony/Controller.mustache

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -180,34 +180,56 @@ class Controller extends AbstractController
180180
*/
181181
protected function getOutputFormat(string $accept, array $produced): ?string
182182
{
183-
// Figure out what the client accepts
184-
$accept = preg_split("/[\s,]+/", $accept);
185-
186-
// Remove q-factor weighting. E.g. "application/json;q=0.8" becomes "application/json"
187-
$accept = array_map(function ($type) {return explode(';', $type)[0];}, $accept);
188-
189-
if (in_array('*/*', $accept) || in_array('application/*', $accept)) {
190-
// Prefer JSON if the client has no preference
191-
if (in_array('application/json', $produced)) {
192-
return 'application/json';
193-
}
194-
if (in_array('application/xml', $produced)) {
195-
return 'application/xml';
183+
// First get the list of formats accepted by the client by weight.
184+
// eg: text/html,*/*; q=0.7, text/*;q=0.8,text/plain;format=fixed;q=0.6 is turned to
185+
// [
186+
// text/html => 1, // because when no weight is present then the default value is 1. See https://httpwg.org/specs/rfc9110.html#quality.values .
187+
// */* => 0.7,
188+
// text/* => 0.8,
189+
// text/plain => 0.6,
190+
// ]
191+
// So we can subsequently order that list by descending weight (because 1 is the most prefered value, 0.001 is the least preferred)
192+
$weightedFormats = array();
193+
foreach (explode(",", str_replace(' ', '', $accept)) as $accept) {
194+
$exploded = explode(';', $accept);
195+
196+
// If no weight is present then the default value is 1 (see https://httpwg.org/specs/rfc9110.html#quality.values )
197+
if (count($exploded) === 1) {
198+
$weight = 1.0;
199+
} else {
200+
$lastItem = end($exploded);
201+
if (str_starts_with($lastItem, "q=")) {
202+
$weight = (float) str_replace("q=", "", $lastItem);
203+
} else {
204+
$weight = 1.0;
205+
}
196206
}
207+
$weightedFormats[$exploded[0]] = $weight;
197208
}
209+
arsort($weightedFormats);
210+
211+
// Now return the first produced format that matches
212+
foreach (array_keys($weightedFormats) as $acceptedFormat) {
213+
$acceptedFormatParts = explode('/', $acceptedFormat);
214+
if (count($acceptedFormatParts) != 2) {
215+
// badly formatted header sent by the client. Let's continue (instead of crashing)
216+
continue;
217+
}
218+
$acceptedFormatType = $acceptedFormatParts[0];
219+
$acceptedFormatSubtype = $acceptedFormatParts[1];
198220
199-
if (in_array('application/json', $accept) && in_array('application/json', $produced)) {
200-
return 'application/json';
201-
}
202-
203-
if (in_array('application/xml', $accept) && in_array('application/xml', $produced)) {
204-
return 'application/xml';
205-
}
206-
207-
if (in_array('*/*', $accept)) {
208-
return $produced[0];
221+
foreach ($produced as $producedFormat) {
222+
if ($acceptedFormat === $producedFormat) {
223+
return $producedFormat;
224+
}
225+
if ($acceptedFormatSubtype === '*' && $acceptedFormatType === explode("/", $producedFormat)[0]) {
226+
return $producedFormat;
227+
}
228+
if ($acceptedFormat === "*/*") {
229+
return $producedFormat;
230+
}
231+
}
209232
}
210-
211233
// If we reach this point, we don't have a common ground between server and client
212234
return null;
213235
}

samples/server/petstore/php-symfony/SymfonyBundle-php/Controller/Controller.php

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -190,34 +190,56 @@ private function exceptionToArray(?\Throwable $exception = null): ?array
190190
*/
191191
protected function getOutputFormat(string $accept, array $produced): ?string
192192
{
193-
// Figure out what the client accepts
194-
$accept = preg_split("/[\s,]+/", $accept);
195-
196-
// Remove q-factor weighting. E.g. "application/json;q=0.8" becomes "application/json"
197-
$accept = array_map(function ($type) {return explode(';', $type)[0];}, $accept);
198-
199-
if (in_array('*/*', $accept) || in_array('application/*', $accept)) {
200-
// Prefer JSON if the client has no preference
201-
if (in_array('application/json', $produced)) {
202-
return 'application/json';
203-
}
204-
if (in_array('application/xml', $produced)) {
205-
return 'application/xml';
193+
// First get the list of formats accepted by the client by weight.
194+
// eg: text/html,*/*; q=0.7, text/*;q=0.8,text/plain;format=fixed;q=0.6 is turned to
195+
// [
196+
// text/html => 1, // because when no weight is present then the default value is 1. See https://httpwg.org/specs/rfc9110.html#quality.values .
197+
// */* => 0.7,
198+
// text/* => 0.8,
199+
// text/plain => 0.6,
200+
// ]
201+
// So we can subsequently order that list by descending weight (because 1 is the most prefered value, 0.001 is the least preferred)
202+
$weightedFormats = array();
203+
foreach (explode(",", str_replace(' ', '', $accept)) as $accept) {
204+
$exploded = explode(';', $accept);
205+
206+
// If no weight is present then the default value is 1 (see https://httpwg.org/specs/rfc9110.html#quality.values )
207+
if (count($exploded) === 1) {
208+
$weight = 1.0;
209+
} else {
210+
$lastItem = end($exploded);
211+
if (str_starts_with($lastItem, "q=")) {
212+
$weight = (float) str_replace("q=", "", $lastItem);
213+
} else {
214+
$weight = 1.0;
215+
}
206216
}
217+
$weightedFormats[$exploded[0]] = $weight;
207218
}
219+
arsort($weightedFormats);
220+
221+
// Now return the first produced format that matches
222+
foreach (array_keys($weightedFormats) as $acceptedFormat) {
223+
$acceptedFormatParts = explode('/', $acceptedFormat);
224+
if (count($acceptedFormatParts) != 2) {
225+
// badly formatted header sent by the client. Let's continue (instead of crashing)
226+
continue;
227+
}
228+
$acceptedFormatType = $acceptedFormatParts[0];
229+
$acceptedFormatSubtype = $acceptedFormatParts[1];
208230

209-
if (in_array('application/json', $accept) && in_array('application/json', $produced)) {
210-
return 'application/json';
211-
}
212-
213-
if (in_array('application/xml', $accept) && in_array('application/xml', $produced)) {
214-
return 'application/xml';
215-
}
216-
217-
if (in_array('*/*', $accept)) {
218-
return $produced[0];
231+
foreach ($produced as $producedFormat) {
232+
if ($acceptedFormat === $producedFormat) {
233+
return $producedFormat;
234+
}
235+
if ($acceptedFormatSubtype === '*' && $acceptedFormatType === explode("/", $producedFormat)[0]) {
236+
return $producedFormat;
237+
}
238+
if ($acceptedFormat === "*/*") {
239+
return $producedFormat;
240+
}
241+
}
219242
}
220-
221243
// If we reach this point, we don't have a common ground between server and client
222244
return null;
223245
}

0 commit comments

Comments
 (0)