diff --git a/src/Metadata/Util/ContentNegotiationTrait.php b/src/Metadata/Util/ContentNegotiationTrait.php index 6690cca704..9f951261a4 100644 --- a/src/Metadata/Util/ContentNegotiationTrait.php +++ b/src/Metadata/Util/ContentNegotiationTrait.php @@ -97,7 +97,15 @@ private function getRequestFormat(Request $request, array $formats, bool $throw /** @var string|null $accept */ $accept = $request->headers->get('Accept'); if (null !== $accept) { - if ($mediaType = $this->negotiator->getBest($accept, $mimeTypes)) { + $mediaType = $this->negotiator->getBest($accept, $mimeTypes); + // willdurand/negotiation treats media-range parameters as match + // constraints, so a wildcard carrying informational params + // (e.g. `*\/*; charset=utf-8` from PhpStorm) fails negotiation. + // Retry on a bare wildcard. See #1532. + if (!$mediaType && str_contains($accept, '*/*')) { + $mediaType = $this->negotiator->getBest('*/*', $mimeTypes); + } + if ($mediaType) { return $this->getMimeTypeFormat($mediaType->getType(), $formats); } diff --git a/tests/Fixtures/TestBundle/ApiResource/WildcardAcceptFormat/WildcardAcceptFormat.php b/tests/Fixtures/TestBundle/ApiResource/WildcardAcceptFormat/WildcardAcceptFormat.php new file mode 100644 index 0000000000..9e84819426 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/WildcardAcceptFormat/WildcardAcceptFormat.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WildcardAcceptFormat; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use Symfony\Component\HttpFoundation\Response; + +#[Get( + shortName: 'WildcardAcceptFormat', + uriTemplate: '/wildcard_accept_format/{id}', + outputFormats: ['jsonld' => ['application/ld+json'], 'html' => ['text/html']], + provider: [self::class, 'provide'], + extraProperties: ['_api_disable_swagger_provider' => true] +)] +class WildcardAcceptFormat +{ + #[ApiProperty(identifier: true)] + public int $id = 1; + + public string $name = 'hello'; + + public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self|Response + { + if ('html' === $context['request']?->getRequestFormat()) { + return new Response('

hello

', 200, ['Content-Type' => 'text/html']); + } + + return new self(); + } +} diff --git a/tests/Functional/FormatTest.php b/tests/Functional/FormatTest.php index 80aab72cf9..4fb21227b9 100644 --- a/tests/Functional/FormatTest.php +++ b/tests/Functional/FormatTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6384\AcceptHtml; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WildcardAcceptFormat\WildcardAcceptFormat; use ApiPlatform\Tests\SetupClassResourcesTrait; final class FormatTest extends ApiTestCase @@ -28,7 +29,7 @@ final class FormatTest extends ApiTestCase */ public static function getResources(): array { - return [AcceptHtml::class]; + return [AcceptHtml::class, WildcardAcceptFormat::class]; } public function testShouldReturnHtml(): void @@ -37,4 +38,37 @@ public function testShouldReturnHtml(): void $this->assertResponseIsSuccessful(); $this->assertEquals($r->getContent(), '

hello

'); } + + /** + * @see https://github.com/api-platform/core/issues/1532 + */ + public function testWildcardAcceptHeaderPicksFirstConfiguredFormat(): void + { + $response = self::createClient()->request('GET', '/wildcard_accept_format/1', ['headers' => ['Accept' => '*/*']]); + $this->assertResponseIsSuccessful(); + $this->assertStringStartsWith('application/ld+json', $response->getHeaders()['content-type'][0]); + } + + /** + * @see https://github.com/api-platform/core/issues/1532 + */ + public function testWildcardAcceptHeaderWithParametersPicksFirstConfiguredFormat(): void + { + $response = self::createClient()->request('GET', '/wildcard_accept_format/1', ['headers' => ['Accept' => '*/*; charset=utf-8']]); + $this->assertResponseIsSuccessful(); + $this->assertStringStartsWith('application/ld+json', $response->getHeaders()['content-type'][0]); + } + + public function testWildcardAcceptHeaderRespectsQualityOfConcreteType(): void + { + $response = self::createClient()->request('GET', '/wildcard_accept_format/1', ['headers' => ['Accept' => '*/*; charset=utf-8; q=0.1, text/html; q=0.9']]); + $this->assertResponseIsSuccessful(); + $this->assertStringStartsWith('text/html', $response->getHeaders()['content-type'][0]); + } + + public function testConcreteAcceptHeaderWithUnsupportedTypeReturnsNotAcceptable(): void + { + self::createClient()->request('GET', '/wildcard_accept_format/1', ['headers' => ['Accept' => 'application/xml']]); + $this->assertResponseStatusCodeSame(406); + } }