diff --git a/core/modules/system/src/Tests/DrupalKernel/ContentNegotiationTest.php b/core/modules/system/src/Tests/DrupalKernel/ContentNegotiationTest.php
index f45f574..bf0231c 100644
--- a/core/modules/system/src/Tests/DrupalKernel/ContentNegotiationTest.php
+++ b/core/modules/system/src/Tests/DrupalKernel/ContentNegotiationTest.php
@@ -12,7 +12,7 @@
 /**
  * Tests content negotiation.
  *
- * @group DrupalKernel
+ * @group ContentNegotiation
  */
 class ContentNegotiationTest extends WebTestBase {
 
diff --git a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
new file mode 100644
index 0000000..027ba52
--- /dev/null
+++ b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\conneg_test\Tests\ContentNegotiationRoutingTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Drupal\simpletest\KernelTestBase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Tests content negotiation routing variations.
+ *
+ * @group ContentNegotiation
+ */
+class ContentNegotiationRoutingTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'conneg_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    \Drupal::unsetContainer();
+    parent::setUp();
+
+    $this->installSchema('system', ['router', 'url_alias']);
+    \Drupal::service('router.builder')->rebuild();
+  }
+
+  function testContentRouting() {
+    $tests = [
+      // ['path', 'accept', 'content-type', 'vary'],
+
+      // Extension is part of the route path. Constant Content-type.
+      ['conneg/simple.json', '', 'application/json', FALSE],
+      ['conneg/simple.json', 'application/xml', 'application/json', FALSE],
+      ['conneg/simple.json', 'application/json', 'application/json', FALSE],
+
+      // No extension. Constant Content-type.
+      ['conneg/html', '', 'text/html', FALSE],
+      ['conneg/html', '*/*', 'text/html', FALSE],
+      // @TODO We have to turn of full content negotiation to support this.
+      ['conneg/html', 'application/xml', 'text/html', FALSE],
+      ['conneg/html', 'text/xml', 'text/html', FALSE],
+      ['conneg/html', 'text/html', 'text/html', FALSE],
+
+      // Dynamic extension. Linked Content-type.
+      // @TODO We have to turn of full content negotiation to support this.
+      ['conneg/html.json', '', 'application/json', FALSE],
+      ['conneg/html.json', '*/*', 'application/json', FALSE],
+      ['conneg/html.json', 'application/xml', 'application/json', FALSE],
+      ['conneg/html.json', 'application/json', 'application/json', FALSE],
+
+      // @TODO We have to turn of full content negotiation to support this.
+      ['conneg/html.xml', '', 'application/xml', FALSE],
+      ['conneg/html.xml', '*/*', 'application/xml', FALSE],
+      ['conneg/html.xml', 'application/json', 'application/xml', FALSE],
+      ['conneg/html.xml', 'application/xml', 'application/xml', FALSE],
+      // @TODO a Drupal vendor protocol?
+
+      // Path with a variable. Variable contains a period.
+      // @TODO We have to turn of full content negotiation to support these.
+      ['conneg/plugin/plugin.id', '', 'text/html', FALSE],
+      ['conneg/plugin/plugin.id', '*/*', 'text/html', FALSE],
+      ['conneg/plugin/plugin.id', 'text/xml', 'text/html', FALSE],
+      ['conneg/plugin/plugin.id', 'text/html', 'text/html', FALSE],
+
+      // Alias with extension pointing to no extension/constant content-type.
+      // @TODO how to test aliasing? Can we just assume aliasing does its thing?
+
+      // Alias with extension pointing to dynamic extension/linked content-type.
+      // @TODO how to test aliasing? Can we just assume aliasing does its thing?
+
+      // Alias with extension pointing to dynamic extension/linked content-type.
+      // @TODO how to test aliasing? Can we just assume aliasing does its thing?
+      // this might not even work :(
+    ];
+
+    foreach ($tests as $test) {
+      $message = "Testing path:$test[0] Accept:$test[1] Content-type:$test[2]";
+      $request = Request::create($test[0]);
+      if ($test[1]) {
+        $request->headers->set('Accept', $test[1]);
+      }
+
+      /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
+      $kernel = \Drupal::getContainer()->get('http_kernel');
+      $response = $kernel->handle($request);
+
+      $this->assertTrue(TRUE, $message); // verbose message since simpletest doesn't let us provide a message and see the error.
+      $this->assertEqual($response->getStatusCode(), Response::HTTP_OK);
+      $this->assertEqual($response->headers->get('Content-type'), $test[2]);
+
+      if ($test[3]) {
+        $this->assertEqual($response->headers->get('Cache-Control'), 'public');
+        $this->assertEqual($response->headers->get('Vary'), 'Cookie');
+      }
+    }
+  }
+
+  /**
+   * Full negotiation by header only.
+   */
+  function testFullNegotiation() {
+    $tests = [
+      // ['path', 'accept', 'content-type'],
+
+//      ['conneg/negotiate', '', 'text/html'], // 406?
+//      ['conneg/negotiate', '', 'text/html'], 406?
+//      ['conneg/negotiate', '*/*', '??'],
+      ['conneg/negotiate', 'application/json', 'application/json'],
+      ['conneg/negotiate', 'application/xml', 'application/xml'],
+      ['conneg/negotiate', 'application/json', 'application/json'],
+      ['conneg/negotiate', 'application/xml', 'application/xml'],
+    ];
+
+    foreach ($tests as $test) {
+      $message = "Testing path:$test[0] Accept:$test[1] Content-type:$test[2]";
+      $request = Request::create($test[0]);
+      $request->headers->set('Accept', $test[1]);
+
+      /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
+      $kernel = \Drupal::getContainer()->get('http_kernel');
+      $response = $kernel->handle($request);
+
+      $this->assertTrue(TRUE, $message); // verbose message since simpletest doesn't let us provide a message and see the error.
+      $this->assertEqual($response->getStatusCode(), Response::HTTP_OK);
+      $this->assertEqual($response->headers->get('Content-type'), $test[2]);
+      $this->assertEqual($response->headers->get('Cache-Control'), 'public');
+      $this->assertEqual($response->headers->get('Vary'), 'Cookie');
+    }
+  }
+
+  /**
+   * Test edge case routing that returns 400 responses.
+   */
+  function testContentRouting400() {
+    $tests = [
+      // ['path', 'accept', 'content-type'],
+      ['conneg/negotiate', 'vnd/dne', 'text/html', Response::HTTP_NOT_ACCEPTABLE],
+      // TODO negotiate a content type no support by the path but supported by error handler.
+
+      ['conneg/html.dne', 'vnd/dne', 'text/html', Response::HTTP_NOT_FOUND],
+    ];
+
+    foreach ($tests as $test) {
+      $message = "Testing path:$test[0] Accept:$test[1] Content-type:$test[2]";
+      $request = Request::create($test[0]);
+      $request->headers->set('Accept', $test[1]);
+
+      /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */
+      $kernel = \Drupal::getContainer()->get('http_kernel');
+      $response = $kernel->handle($request);
+
+      $this->assertTrue(TRUE, $message); // verbose message since simpletest doesn't let us provide a message and see the error.
+      $this->assertEqual($response->getStatusCode(), $test[3]);
+      $this->assertEqual($response->headers->get('Content-type'), $test[2]);
+    }
+  }
+}
diff --git a/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml b/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml
new file mode 100644
index 0000000..07f4600
--- /dev/null
+++ b/core/modules/system/tests/modules/conneg_test/conneg_test.info.yml
@@ -0,0 +1,6 @@
+name: Content negotiation test
+type: module
+description: 'Support testing content negotiation variations.'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/conneg_test/conneg_test.module b/core/modules/system/tests/modules/conneg_test/conneg_test.module
new file mode 100644
index 0000000..c1c3699
--- /dev/null
+++ b/core/modules/system/tests/modules/conneg_test/conneg_test.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Helper module for the content negotiation tests.
+ */
diff --git a/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml b/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml
new file mode 100644
index 0000000..fec9e88
--- /dev/null
+++ b/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml
@@ -0,0 +1,32 @@
+# Tests
+conneg.simpletest:
+  path: conneg/simple.json
+  defaults:
+    _controller: '\Drupal\conneg_test\Controller\Test::simple'
+  requirements:
+    _access: 'TRUE'
+conneg.html:
+  path: conneg/html
+  defaults:
+    _controller: '\Drupal\conneg_test\Controller\Test::html'
+  requirements:
+    _access: 'TRUE'
+conneg.simple_conneg:
+  path: conneg/html.{_format}
+  defaults:
+    _controller: '\Drupal\conneg_test\Controller\Test::format'
+  requirements:
+    _access: 'TRUE'
+    _format: 'json|xml'
+conneg.variable_with_period:
+  path: conneg/plugin/{plugin_id}
+  defaults:
+    _controller: '\Drupal\conneg_test\Controller\Test::variable'
+  requirements:
+    _access: 'TRUE'
+conneg.full_content_negotiation:
+  path: conneg/negotiate
+  defaults:
+    _controller: '\Drupal\conneg_test\Controller\Test::format'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/conneg_test/src/Controller/Test.php b/core/modules/system/tests/modules/conneg_test/src/Controller/Test.php
new file mode 100644
index 0000000..ea88048
--- /dev/null
+++ b/core/modules/system/tests/modules/conneg_test/src/Controller/Test.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\conneg_test\Controller\Test.
+ */
+
+namespace Drupal\conneg_test\Controller;
+
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class Test {
+  public function simple() {
+    return new JsonResponse(['some' => 'data']);
+  }
+
+  public function html() {
+    return [
+      '#markup' => 'here',
+    ];
+  }
+
+  public function format(Request $request) {
+    switch ($request->getRequestFormat()) {
+      case 'json':
+        return new JsonResponse(['some' => 'data']);
+
+      case 'xml':
+        return new Response('<xml></xml>', Response::HTTP_OK, ['Content-Type' => 'application/xml']);
+
+      default:
+        var_dump($request->getRequestFormat());
+    }
+  }
+
+  public function variable($plugin_id) {
+    return [
+      '#markup' => $plugin_id,
+    ];
+  }
+}
