diff --git a/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php index 8d14dd52cf4c98ac698404da41816819ef67d636..fad7bc04659f0ef4dedc87741a08eae75bfcb79b 100644 --- a/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php +++ b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Routing; +use Drupal\Core\Url; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\Routing\Route; @@ -60,7 +61,18 @@ public function filter(RouteCollection $collection, Request $request) { // We do not throw a // \Symfony\Component\Routing\Exception\ResourceNotFoundException here // because we don't want to return a 404 status code, but rather a 406. - throw new NotAcceptableHttpException("No route found for the specified format $format."); + $available_formats = static::getAvailableFormats($collection); + $not_acceptable = new NotAcceptableHttpException("No route found for the specified format $format. Supported formats: " . implode(', ', $available_formats) . '.'); + if ($available_formats) { + $links = []; + foreach ($available_formats as $available_format) { + $url = Url::fromUri($request->getUri(), ['query' => ['_format' => $available_format]])->toString(TRUE)->getGeneratedUrl(); + $content_type = $request->getMimeType($available_format); + $links[] = "<$url>; rel=\"alternate\"; type=\"$content_type\""; + } + $not_acceptable->setHeaders(['Link' => implode(', ', $links)]); + } + throw $not_acceptable; } /** @@ -79,7 +91,24 @@ public function filter(RouteCollection $collection, Request $request) { * The default format. */ protected static function getDefaultFormat(RouteCollection $collection) { - // Get the set of formats across all routes in the collection. + $formats = static::getAvailableFormats($collection); + + // The default format is 'html' unless ALL routes require the same format. + return count($formats) === 1 + ? reset($formats) + : 'html'; + } + + /** + * Gets the set of formats across all routes in the collection. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection to filter. + * + * @return string[] + * All available formats. + */ + protected static function getAvailableFormats(RouteCollection $collection) { $all_formats = array_reduce($collection->all(), function (array $carry, Route $route) { // Routes without a '_format' requirement are assumed to require HTML. $route_formats = !$route->hasRequirement('_format') @@ -87,12 +116,7 @@ protected static function getDefaultFormat(RouteCollection $collection) { : explode('|', $route->getRequirement('_format')); return array_merge($carry, $route_formats); }, []); - $formats = array_unique(array_filter($all_formats)); - - // The default format is 'html' unless ALL routes require the same format. - return count($formats) === 1 - ? reset($formats) - : 'html'; + return array_unique(array_filter($all_formats)); } } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 76f0139be75db7684b196c4c9e99f78beb2195e0..6f84d7e131c01ed42eb4aad0bd76ee7ff16189a3 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -1492,6 +1492,8 @@ protected function assert406Response(ResponseInterface $response) { else { // This is the desired response. $this->assertSame(406, $response->getStatusCode()); + $this->stringContains('?_format=' . static::$format . '>; rel="alternate"; type="' . static::$mimeType . '"', $response->getHeader('Link')); + $this->stringContains('?_format=foobar>; rel="alternate"', $response->getHeader('Link')); } } diff --git a/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php b/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php index 3574ee195004ca24718284846eefd19e9f4341c9..83d0505f15c765a2dbf3d4af011c2d2a9cb7ca81 100644 --- a/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php +++ b/core/tests/Drupal/Tests/Core/Routing/RequestFormatRouteFilterTest.php @@ -2,7 +2,10 @@ namespace Drupal\Tests\Core\Routing; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\GeneratedUrl; use Drupal\Core\Routing\RequestFormatRouteFilter; +use Drupal\Core\Utility\UnroutedUrlAssemblerInterface; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; @@ -59,6 +62,14 @@ public function filterProvider() { * @covers ::filter */ public function testNoRouteFound() { + $url = $this->prophesize(GeneratedUrl::class); + $url_assembler = $this->prophesize(UnroutedUrlAssemblerInterface::class); + $url_assembler->assemble('http://localhost/test?_format=xml', ['query' => ['_format' => 'json'], 'external' => TRUE], TRUE) + ->willReturn($url); + $container = new ContainerBuilder(); + $container->set('unrouted_url_assembler', $url_assembler->reveal()); + \Drupal::setContainer($container); + $collection = new RouteCollection(); $route_with_format = $route = new Route('/test'); $route_with_format->setRequirement('_format', 'json'); @@ -78,6 +89,16 @@ public function testNoRouteFound() { public function testNoRouteFoundWhenNoRequestFormatAndSingleRouteWithMultipleFormats() { $this->setExpectedException(NotAcceptableHttpException::class, 'No route found for the specified format html.'); + $url = $this->prophesize(GeneratedUrl::class); + $url_assembler = $this->prophesize(UnroutedUrlAssemblerInterface::class); + $url_assembler->assemble('http://localhost/test', ['query' => ['_format' => 'json'], 'external' => TRUE], TRUE) + ->willReturn($url); + $url_assembler->assemble('http://localhost/test', ['query' => ['_format' => 'xml'], 'external' => TRUE], TRUE) + ->willReturn($url); + $container = new ContainerBuilder(); + $container->set('unrouted_url_assembler', $url_assembler->reveal()); + \Drupal::setContainer($container); + $collection = new RouteCollection(); $route_with_format = $route = new Route('/test'); $route_with_format->setRequirement('_format', 'json|xml');