From 3e80a509c5a554547af460f955f6666c25db8592 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 09:32:12 +0200 Subject: [PATCH 1/2] fix(test): default content-type in ApiTestCase matches configured formats Symfony's HttpClient automatically sets "Content-Type: application/json" when the request uses the "json" option. On a default API Platform project (no explicit formats configured) only jsonld is registered, so such requests fail with 415 Unsupported Media Type. ApiTestCase now injects a default Content-Type header derived from the first configured api_platform.formats entry whenever the caller did not provide one, keeping out-of-the-box test ergonomics consistent with the configured formats while still letting users override the header explicitly. Fixes #7670 --- src/Symfony/Bundle/Test/ApiTestCase.php | 39 ++++++++++++++++++- tests/Symfony/Bundle/Test/ApiTestCaseTest.php | 23 +++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/Test/ApiTestCase.php b/src/Symfony/Bundle/Test/ApiTestCase.php index 891b5f09ab9..3b0b30315cd 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,43 @@ 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. + * To make tests work out of the box, default the Content-Type to the first configured API Platform format + * when the caller did not provide one. + */ + 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; + } + + $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..d8a13c06451 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -403,6 +403,29 @@ public function testMissingMethod(): void $this->assertResponseStatusCodeSame(404); } + public function testDefaultContentTypeMatchesFirstConfiguredFormat(): void + { + $client = self::createClient(); + $client->request('POST', '/something/that/does/not/exist/ever', ['json' => ['name' => 'Kevin']]); + + $contentType = $client->getKernelBrowser()->getRequest()->headers->get('Content-Type'); + $formats = self::getContainer()->getParameter('api_platform.formats'); + $firstFormatMimeType = reset($formats)[0]; + + $this->assertSame($firstFormatMimeType, $contentType); + } + + public function testExplicitContentTypeOverridesDefault(): 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; From d42777ec42d3831bc2b26b00e930cd9f240bf028 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 3 Jun 2026 12:00:16 +0200 Subject: [PATCH 2/2] fix(test): skip default content-type when application/json is configured The previous commit injected a default Content-Type from the first configured api_platform.formats entry on every ApiTestCase request. Because Symfony's HttpClient merges default headers before the per-request "json" option resolves its implicit "Content-Type: application/json", that default ended up overriding application/json for callers that legitimately want it - notably the GraphQL test trait (POST /graphql with ['json' => $payload]), which then received "application/ld+json" and was rejected with a 400. Skip the default whenever "application/json" is among the configured mime types: in that case Symfony's implicit header already produces a valid request, so preserving it keeps GraphQL and any "json"-format project working. The original out-of-the-box case (default API Platform project with only jsonld configured) is still covered because application/json is not in formats there. --- src/Symfony/Bundle/Test/ApiTestCase.php | 17 ++++++++++++++--- tests/Symfony/Bundle/Test/ApiTestCaseTest.php | 13 ++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bundle/Test/ApiTestCase.php b/src/Symfony/Bundle/Test/ApiTestCase.php index 3b0b30315cd..90bd2a7db96 100644 --- a/src/Symfony/Bundle/Test/ApiTestCase.php +++ b/src/Symfony/Bundle/Test/ApiTestCase.php @@ -115,9 +115,14 @@ protected static function createClient(array $kernelOptions = [], array $default /** * 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. - * To make tests work out of the box, default the Content-Type to the first configured API Platform format - * when the caller did not provide one. + * 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 { @@ -138,6 +143,12 @@ private static function withDefaultContentType(array $defaultOptions): array 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) { diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index d8a13c06451..3756bb92998 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -403,19 +403,18 @@ public function testMissingMethod(): void $this->assertResponseStatusCodeSame(404); } - public function testDefaultContentTypeMatchesFirstConfiguredFormat(): void + 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']]); - $contentType = $client->getKernelBrowser()->getRequest()->headers->get('Content-Type'); - $formats = self::getContainer()->getParameter('api_platform.formats'); - $firstFormatMimeType = reset($formats)[0]; - - $this->assertSame($firstFormatMimeType, $contentType); + $this->assertSame('application/json', $client->getKernelBrowser()->getRequest()->headers->get('Content-Type')); } - public function testExplicitContentTypeOverridesDefault(): void + public function testExplicitContentTypeIsPreserved(): void { $client = self::createClient(); $client->request('POST', '/something/that/does/not/exist/ever', [