diff --git a/src/Symfony/Bundle/Test/ApiTestCase.php b/src/Symfony/Bundle/Test/ApiTestCase.php index 891b5f09ab9..90bd2a7db96 100644 --- a/src/Symfony/Bundle/Test/ApiTestCase.php +++ b/src/Symfony/Bundle/Test/ApiTestCase.php @@ -105,7 +105,7 @@ protected static function createClient(array $kernelOptions = [], array $default throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.'); } - $client->setDefaultOptions($defaultOptions); + $client->setDefaultOptions(self::withDefaultContentType($defaultOptions)); self::getHttpClient($client); self::getClient($client->getKernelBrowser()); @@ -113,6 +113,54 @@ protected static function createClient(array $kernelOptions = [], array $default return $client; } + /** + * Symfony's HttpClient adds "Content-Type: application/json" automatically when the "json" option is used. + * On a default API Platform project, "json" is not part of the configured formats, so such requests fail + * with 415 Unsupported Media Type. To keep tests working out of the box in that scenario, default the + * Content-Type to the first configured API Platform format. + * + * The default is only applied when "application/json" is NOT one of the configured mime types: when it is + * (e.g. a project that explicitly enables the "json" format, or any GraphQL endpoint that accepts + * "application/json" regardless of the API Platform formats), Symfony's implicit "application/json" + * header already produces a valid request, and overriding it would break per-request "json" usage. + */ + private static function withDefaultContentType(array $defaultOptions): array + { + $headers = $defaultOptions['headers'] ?? []; + foreach (array_keys($headers) as $name) { + if (\is_string($name) && 0 === strcasecmp($name, 'content-type')) { + return $defaultOptions; + } + } + + $container = self::getContainer(); + if (!$container->hasParameter('api_platform.formats')) { + return $defaultOptions; + } + + $formats = $container->getParameter('api_platform.formats'); + if (!\is_array($formats) || !$formats) { + return $defaultOptions; + } + + foreach ($formats as $mimeTypes) { + if (\is_array($mimeTypes) && \in_array('application/json', $mimeTypes, true)) { + return $defaultOptions; + } + } + + $firstFormat = reset($formats); + $mimeType = \is_array($firstFormat) ? ($firstFormat[0] ?? null) : null; + if (!\is_string($mimeType) || '' === $mimeType) { + return $defaultOptions; + } + + $headers['content-type'] = $mimeType; + $defaultOptions['headers'] = $headers; + + return $defaultOptions; + } + /** * Finds the IRI of a resource item matching the resource class and the specified criteria. */ diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index 422d1e558c6..3756bb92998 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -403,6 +403,28 @@ public function testMissingMethod(): void $this->assertResponseStatusCodeSame(404); } + public function testJsonOptionContentTypePreservedWhenApplicationJsonConfigured(): void + { + // The test fixtures configure "application/json" among the API Platform formats, so Symfony's + // implicit "Content-Type: application/json" header (set by the "json" option) must not be overridden. + // Overriding it would break callers that legitimately rely on application/json (e.g. /graphql). + $client = self::createClient(); + $client->request('POST', '/something/that/does/not/exist/ever', ['json' => ['name' => 'Kevin']]); + + $this->assertSame('application/json', $client->getKernelBrowser()->getRequest()->headers->get('Content-Type')); + } + + public function testExplicitContentTypeIsPreserved(): void + { + $client = self::createClient(); + $client->request('POST', '/something/that/does/not/exist/ever', [ + 'headers' => ['content-type' => 'application/json'], + 'body' => '{"name":"Kevin"}', + ]); + + $this->assertSame('application/json', $client->getKernelBrowser()->getRequest()->headers->get('Content-Type')); + } + public function testDoNotRebootKernelOnCreateClient(): void { self::$alwaysBootKernel = false;