From 8ce2a3ec3a67a6b8a9cc5b74e1ec47da484ccf3a Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Sat, 2 Jun 2018 17:40:15 +0200 Subject: [PATCH] Issue #2955383 by Wim Leers, borisson_, dawehner: List available representations in 406 responses --- .../Core/Routing/RequestFormatRouteFilter.php | 40 +++++++++++++++---- .../EntityResource/EntityResourceTestBase.php | 2 + .../Routing/RequestFormatRouteFilterTest.php | 21 ++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php index 8d14dd52cf..fad7bc0465 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 76f0139be7..6f84d7e131 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 3574ee1950..83d0505f15 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'); -- GitLab