diff --git a/core/core.services.yml b/core/core.services.yml
index 891d0b7..e7e4f37 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -768,8 +768,12 @@ services:
   password:
     class: Drupal\Core\Password\PhpassHashedPassword
     arguments: [16]
-  accept_header_matcher:
-    class: Drupal\Core\Routing\AcceptHeaderMatcher
+#  accept_header_matcher:
+#    class: Drupal\Core\Routing\AcceptHeaderMatcher
+#    tags:
+#      - { name: route_filter }
+  request_format_matcher:
+    class: Drupal\Core\Routing\RequestFormatMatcher
     tags:
       - { name: route_filter }
   content_type_header_matcher:
@@ -1054,6 +1058,11 @@ services:
     arguments: ['@current_route_match']
     tags:
       - { name: route_processor_outbound, priority: 200 }
+  route_processor_request_format:
+    class: Drupal\Core\RouteProcessor\RouteProcessorRequestFormat
+    arguments: ['@request_stack']
+    tags:
+      - { name: route_processor_outbound }
   path_processor_alias:
     class: Drupal\Core\PathProcessor\PathProcessorAlias
     tags:
diff --git a/core/lib/Drupal/Core/ContentNegotiation.php b/core/lib/Drupal/Core/ContentNegotiation.php
index 857f9d5..6d9e1be 100644
--- a/core/lib/Drupal/Core/ContentNegotiation.php
+++ b/core/lib/Drupal/Core/ContentNegotiation.php
@@ -36,22 +36,8 @@ public function getContentType(Request $request) {
       return 'iframeupload';
     }
 
-    // Check all formats, if priority format is found return it.
-    $first_found_format = FALSE;
-    $priority = array('html', 'drupal_ajax', 'drupal_modal', 'drupal_dialog');
-    foreach ($request->getAcceptableContentTypes() as $mime_type) {
-      $format = $request->getFormat($mime_type);
-      if (in_array($format, $priority, TRUE)) {
-        return $format;
-      }
-      if (!is_null($format) && !$first_found_format) {
-        $first_found_format = $format;
-      }
-    }
-
-    // No HTML found, return first found.
-    if ($first_found_format) {
-      return $first_found_format;
+    if ($request->query->has('_format')) {
+      return $request->query->get('_format');
     }
 
     if ($request->isXmlHttpRequest()) {
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
index e0a1343..6a3e6f2 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
@@ -204,7 +204,7 @@ protected function getFormat(Request $request) {
     // to this code. We therefore use this style for now on the expectation
     // that it will get replaced with better code later. This approach makes
     // that change easier when we get to it.
-    $format = \Drupal::service('http_negotiation.format_negotiator')->getContentType($request);
+    $format = $request->query->get(MainContentViewSubscriber::WRAPPER_ENVELOPE) ?: \Drupal::service('http_negotiation.format_negotiator')->getContentType($request);
 
     // These are all JSON errors for our purposes. Any special handling for
     // them can/should happen in earlier listeners if desired.
diff --git a/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php
index a14d327..d6143be 100644
--- a/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php
+++ b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php
@@ -86,11 +86,12 @@ public function onException(GetResponseForExceptionEvent $event) {
     $exception = $event->getException();
 
     // Make the exception available for example when rendering a block.
-    $event->getRequest()->attributes->set('exception', $exception);
+    $request = $event->getRequest();
+    $request->attributes->set('exception', $exception);
 
     $handled_formats = $this->getHandledFormats();
 
-    $format = $event->getRequest()->getRequestFormat();
+    $format = $request->query->get(MainContentViewSubscriber::WRAPPER_ENVELOPE) ?: $request->getRequestFormat();
 
     if ($exception instanceof HttpExceptionInterface && (empty($handled_formats) || in_array($format, $handled_formats))) {
       $method = 'on' . $exception->getStatusCode();
diff --git a/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
index fcc51a3..cbdce65 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
@@ -51,6 +51,13 @@ class MainContentViewSubscriber implements EventSubscriberInterface {
   protected $mainContentRenderers;
 
   /**
+   * Url query attribute to indicate the wrapper used to render a request.
+   *
+   * This controls wrapping the HTML content for example in a modal dialog.
+   */
+  const WRAPPER_ENVELOPE = '_wrapper_format';
+
+  /**
    * Constructs a new MainContentViewSubscriber object.
    *
    * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
@@ -76,20 +83,22 @@ public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
     $request = $event->getRequest();
     $result = $event->getControllerResult();
 
-    $format = $request->getRequestFormat();
-
     // Render the controller result into a response if it's a render array.
     if (is_array($result)) {
-      if (isset($this->mainContentRenderers[$format])) {
-        $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$format]);
+      // We default to getRequestFormat() because content negotiation uses this as
+      // its fallback method for displaying a 406 when we fail to match a route
+      // based on format.
+      // @todo this logic should be replaced with a more clear opt in to avoid
+      //    cache poisoning. https://www.drupal.org/node/2364011
+      $wrapper = $request->query->get(static::WRAPPER_ENVELOPE, $request->getRequestFormat());
+      if (isset($this->mainContentRenderers[$wrapper])) {
+        $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]);
         $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch));
       }
       else {
-        $supported_formats = array_keys($this->mainContentRenderers);
-        $supported_mimetypes = array_map([$request, 'getMimeType'], $supported_formats);
         $event->setResponse(new JsonResponse([
           'message' => 'Not Acceptable.',
-          'supported_mime_types' => $supported_mimetypes,
+          'supported_wrapper_envelopes' => array_keys($this->mainContentRenderers),
         ], 406));
       }
     }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index 0283333..b9de164 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Render\Element;
@@ -240,7 +241,7 @@ public static function preRenderAjaxForm($element) {
       $settings += array(
         'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
         'options' => array(),
-        'accepts' => 'application/vnd.drupal-ajax'
+        MainContentViewSubscriber::WRAPPER_ENVELOPE => 'ajax',
       );
 
       // @todo Legacy support. Remove in Drupal 8.
diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorRequestFormat.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorRequestFormat.php
new file mode 100644
index 0000000..c3a1aac
--- /dev/null
+++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorRequestFormat.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RouteProcessor\RouteProcessorRequestFormat.
+ */
+
+namespace Drupal\Core\RouteProcessor;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds the request format to the generated URL.
+ */
+class RouteProcessorRequestFormat implements OutboundRouteProcessorInterface {
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new RouteProcessorRequestFormat instance.
+
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(RequestStack $request_stack) {
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processOutbound($route_name, Route $route, array &$parameters) {
+    if (!array_key_exists('_format', $parameters) && ($request = $this->requestStack->getCurrentRequest()) && ($format = $request->getRequestFormat('html')) && $format !== 'html') {
+      $parameters['_format'] = $format;
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
index 7794800..f725fcc 100644
--- a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
+++ b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
@@ -68,6 +68,7 @@ public function filter(RouteCollection $collection, Request $request) {
    * {@inheritdoc}
    */
   public function applies(Route $route) {
+    // @FIXME Turn it off for now.
     return TRUE;
   }
 
diff --git a/core/lib/Drupal/Core/Routing/RequestFormatMatcher.php b/core/lib/Drupal/Core/Routing/RequestFormatMatcher.php
new file mode 100644
index 0000000..d27fc62
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RequestFormatMatcher.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Routing\RequestFormatMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+class RequestFormatMatcher implements RouteFilterInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return $route->hasRequirement('_format');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(RouteCollection $collection, Request $request) {
+    if ($format = $request->getRequestFormat('html')) {
+      foreach ($collection as $name => $route) {
+        // If its not a match, we move the route to the end, so we effectively
+        // Move the matching route to the front.
+        $supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
+        if (!in_array($format, $supported_formats)) {
+          $collection->add($name, $route);
+        }
+      }
+    }
+    return $collection;
+  }
+
+}
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index e2aeab1..1e26c9d 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -17,6 +17,13 @@
   Drupal.ajax = Drupal.ajax || {};
 
   /**
+   * Url query attribute to indicate the wrapper used to render a request.
+   *
+   * This controls wrapping the HTML content for example in a modal dialog.
+   */
+  Drupal.ajax.WRAPPER_ENVELOPE = '_wrapper_format';
+
+  /**
    * Attaches the Ajax behavior to each Ajax form element.
    */
   Drupal.behaviors.AJAX = {
@@ -52,7 +59,7 @@
           element_settings.url = $(this).attr('href');
           element_settings.event = 'click';
         }
-        element_settings.accepts = $(this).data('accepts');
+        element_settings.dialogType = $(this).data('dialog-type');
         element_settings.dialog = $(this).data('dialog-options');
         var baseUseAjax = $(this).attr('id');
         Drupal.ajax[baseUseAjax] = new Drupal.ajax(baseUseAjax, this, element_settings);
@@ -256,9 +263,6 @@
         }
       },
       dataType: 'json',
-      accepts: {
-        json: element_settings.accepts || 'application/vnd.drupal-ajax'
-      },
       type: 'POST'
     };
 
@@ -266,6 +270,18 @@
       ajax.options.data.dialogOptions = element_settings.dialog;
     }
 
+    if (element_settings.dialogType) {
+      // Ensure that we have a valid URL by adding ? when no query parameter is
+      // yet available, otherwise append using &.
+      if (ajax.options.url.indexOf('?') === -1) {
+        ajax.options.url += '?';
+      }
+      else {
+        ajax.options.url += '&';
+      }
+      ajax.options.url += Drupal.ajax.WRAPPER_ENVELOPE + '=drupal_' + element_settings.dialogType;
+    }
+
     // Bind the ajaxSubmit function to the element event.
     $(ajax.element).on(element_settings.event, function (event) {
       return ajax.eventResponse(this, event);
diff --git a/core/misc/progress.js b/core/misc/progress.js
index 7305d24..b767dc3 100644
--- a/core/misc/progress.js
+++ b/core/misc/progress.js
@@ -83,11 +83,18 @@
         var pb = this;
         // When doing a post request, you need non-null data. Otherwise a
         // HTTP 411 or HTTP 406 (with Apache mod_security) error may result.
+        var uri = this.uri;
+        if (uri === -1) {
+          uri += '?';
+        }
+        else {
+          uri += '&';
+        }
+        uri += '_format=json';
         $.ajax({
           type: this.method,
-          url: this.uri,
+          url: uri,
           data: '',
-          dataType: 'json',
           success: function (progress) {
             // Display errors.
             if (progress.status === 0) {
diff --git a/core/modules/block/src/BlockListBuilder.php b/core/modules/block/src/BlockListBuilder.php
index 6521287..913b718 100644
--- a/core/modules/block/src/BlockListBuilder.php
+++ b/core/modules/block/src/BlockListBuilder.php
@@ -362,7 +362,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         ]),
         'attributes' => array(
           'class' => array('use-ajax', 'block-filter-text-source'),
-          'data-accepts' => 'application/vnd.drupal-modal',
+          'data-dialog-type' => 'modal',
           'data-dialog-options' => Json::encode(array(
             'width' => 700,
           )),
diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js
index 1596808..525f8d2 100644
--- a/core/modules/ckeditor/js/ckeditor.js
+++ b/core/modules/ckeditor/js/ckeditor.js
@@ -160,8 +160,8 @@
       // a Drupal.ajax instance to load the dialog and trigger it.
       var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a>' + Drupal.t('Loading...') + '</a></span></div>');
       $content.appendTo($target);
-      new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), {
-        accepts: 'application/vnd.drupal-modal',
+
+      var options = {
         dialog: dialogSettings,
         selector: '.ckeditor-dialog-loading-link',
         url: url,
@@ -170,7 +170,9 @@
         submit: {
           editor_object: existingValues
         }
-      });
+      };
+      options[Drupal.ajax.WRAPPER_ENVELOPE] = 'modal';
+      new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), options);
       $content.find('a')
         .on('click', function () { return false; })
         .trigger('ckeditor-internal.ckeditor');
diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php
index b97218b..0a91758 100644
--- a/core/modules/config/src/Form/ConfigSync.php
+++ b/core/modules/config/src/Form/ConfigSync.php
@@ -281,7 +281,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
             'url' => Url::fromRoute($route_name, $route_options),
             'attributes' => array(
               'class' => array('use-ajax'),
-              'data-accepts' => 'application/vnd.drupal-modal',
+              'data-dialog-type' => 'modal',
               'data-dialog-options' => json_encode(array(
                 'width' => 700
               )),
diff --git a/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php b/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php
index c4b81ea..b4c29d9 100644
--- a/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php
+++ b/core/modules/editor/src/Tests/QuickEditIntegrationLoadingTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\editor\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -88,7 +89,7 @@ public function testUsersWithoutPermission() {
       $this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
 
       // Retrieving the untransformed text should result in an empty 403 response.
-      $response = $this->drupalPost('editor/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', array());
+      $response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), ['query' => [MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_ajax']]);
       $this->assertResponse(403);
       $this->assertIdentical('{}', $response);
     }
diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php
index 9cc60b1..484c48e 100644
--- a/core/modules/rest/src/Routing/ResourceRoutes.php
+++ b/core/modules/rest/src/Routing/ResourceRoutes.php
@@ -19,7 +19,7 @@
 /**
  * Subscriber for REST-style routes.
  */
-class ResourceRoutes extends RouteSubscriberBase{
+class ResourceRoutes extends RouteSubscriberBase {
 
   /**
    * The plugin manager for REST plugins.
diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php
index 09537c8..f32a86c 100644
--- a/core/modules/rest/src/Tests/AuthTest.php
+++ b/core/modules/rest/src/Tests/AuthTest.php
@@ -38,7 +38,7 @@ public function testRead() {
     $entity->save();
 
     // Try to read the resource as an anonymous user, which should not work.
-    $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
     $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.');
     $this->assertRaw(json_encode(['error' => 'A fatal error occurred: No authentication credentials provided.']));
 
@@ -55,7 +55,7 @@ public function testRead() {
 
     // Try to read the resource with session cookie authentication, which is
     // not enabled and should not work.
-    $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
     $this->assertResponse('403', 'HTTP response code is 403 when the request was authenticated by the wrong authentication provider.');
 
     // Ensure that cURL settings/headers aren't carried over to next request.
@@ -63,7 +63,7 @@ public function testRead() {
 
     // Now read it with the Basic authentication which is enabled and should
     // work.
-    $this->basicAuthGet($entity->urlInfo(), $account->getUsername(), $account->pass_raw, $this->defaultMimeType);
+    $this->basicAuthGet($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), $account->getUsername(), $account->pass_raw);
     $this->assertResponse('200', 'HTTP response code is 200 for successfully authenticated requests.');
     $this->curlClose();
   }
diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php
index 7169102..bf838d4 100644
--- a/core/modules/rest/src/Tests/NodeTest.php
+++ b/core/modules/rest/src/Tests/NodeTest.php
@@ -51,14 +51,14 @@ public function testNodes() {
 
     $node = $this->entityCreate('node');
     $node->save();
-    $this->httpRequest($node->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest($node->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
     $this->assertResponse(200);
     $this->assertHeader('Content-type', $this->defaultMimeType);
 
     // Also check that JSON works and the routing system selects the correct
     // REST route.
     $this->enableService('entity:node', 'GET', 'json');
-    $this->httpRequest($node->urlInfo(), 'GET', NULL, 'application/json');
+    $this->httpRequest($node->urlInfo('canonical', ['query' => ['_format' => 'json']]), 'GET');
     $this->assertResponse(200);
     $this->assertHeader('Content-type', 'application/json');
 
diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php
index d5f2fc1..6031f20 100644
--- a/core/modules/rest/src/Tests/ReadTest.php
+++ b/core/modules/rest/src/Tests/ReadTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\rest\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
 use Drupal\rest\Tests\RESTTestBase;
 
 /**
@@ -44,7 +45,7 @@ public function testRead() {
       $entity = $this->entityCreate($entity_type);
       $entity->save();
       // Read it over the REST API.
-      $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
       $this->assertResponse('200', 'HTTP response code is correct.');
       $this->assertHeader('content-type', $this->defaultMimeType);
       $data = Json::decode($response);
@@ -53,12 +54,12 @@ public function testRead() {
       $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
 
       // Try to read the entity with an unsupported mime format.
-      $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, 'application/wrongformat');
-      $this->assertResponse(200);
-      $this->assertHeader('Content-type', 'text/html; charset=UTF-8');
+      $response = $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => 'wrongformat']]), 'GET');
+      $this->assertResponse(406);
+      $this->assertHeader('Content-type', 'application/json');
 
       // Try to read an entity that does not exist.
-      $response = $this->httpRequest($entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest(Url::fromUri('base://' . $entity_type . '/9999', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
       $this->assertResponse(404);
       $path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}';
       $expected_message = Json::encode(['error' => 'A fatal error occurred: The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
@@ -70,7 +71,7 @@ public function testRead() {
       if ($entity_type == 'entity_test') {
         $entity->field_test_text->value = 'no access value';
         $entity->save();
-        $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+        $response = $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
         $this->assertResponse(200);
         $this->assertHeader('content-type', $this->defaultMimeType);
         $data = Json::decode($response);
@@ -79,20 +80,20 @@ public function testRead() {
 
       // Try to read an entity without proper permissions.
       $this->drupalLogout();
-      $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
       $this->assertResponse(403);
       $this->assertIdentical('{}', $response);
     }
     // Try to read a resource which is not REST API enabled.
     $account = $this->drupalCreateUser();
     $this->drupalLogin($account);
-    $response = $this->httpRequest($account->urlInfo(), 'GET', NULL, $this->defaultMimeType);
+    $response = $this->httpRequest($account->urlInfo('canonical', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
     // AcceptHeaderMatcher considers the canonical, non-REST route a match, but
     // a lower quality one: no format restrictions means there's always a match,
     // and hence when there is no matching REST route, the non-REST route is
     // used, but it can't render into application/hal+json, so it returns a 406.
     $this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
-    $this->assertTrue(strpos($response, '{"message":"Not Acceptable.","supported_mime_types":') !== FALSE);
+    $this->assertTrue(strpos($response, '{"message":"Not Acceptable.","supported_wrapper_envelopes":') !== FALSE);
   }
 
   /**
@@ -113,7 +114,7 @@ public function testResourceStructure() {
     $entity->save();
 
     // Read it over the REST API.
-    $response = $this->httpRequest($entity->urlInfo(), 'GET', NULL, 'application/json');
+    $response = $this->httpRequest($entity->urlInfo('canonical', ['query' => ['_format' => 'json']]), 'GET');
     $this->assertResponse('200', 'HTTP response code is correct.');
   }
 
diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php
index be02617..5144ae3 100644
--- a/core/modules/rest/src/Tests/ResourceTest.php
+++ b/core/modules/rest/src/Tests/ResourceTest.php
@@ -54,7 +54,7 @@ public function testFormats() {
     $this->config->save();
     $this->rebuildCache();
 
-    // Verify that accessing the resource returns 401.
+    // Verify that accessing the resource returns 406.
     $response = $this->httpRequest($this->entity->urlInfo(), 'GET', NULL, $this->defaultMimeType);
     // AcceptHeaderMatcher considers the canonical, non-REST route a match, but
     // a lower quality one: no format restrictions means there's always a match,
diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
index 137e8f0..9806579 100644
--- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
+++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
@@ -76,7 +76,7 @@ public function testSerializerResponses() {
     $view->initDisplay();
     $this->executeView($view);
 
-    $actual_json = $this->drupalGet('test/serialize/field', array(), array('Accept: application/json'));
+    $actual_json = $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'json']));
     $this->assertResponse(200);
     $this->assertCacheTags($view->getCacheTags());
     // @todo Due to https://www.drupal.org/node/2352009 we can't yet test the
@@ -126,7 +126,7 @@ public function testSerializerResponses() {
 
     $expected = $serializer->serialize($entities, 'json');
 
-    $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/json'));
+    $actual_json = $this->drupalGet('test/serialize/entity', array('query' => ['_format' => 'json']));
     $this->assertResponse(200);
     $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
     $expected_cache_tags = $view->getCacheTags();
@@ -138,7 +138,7 @@ public function testSerializerResponses() {
     $this->assertCacheTags($expected_cache_tags);
 
     $expected = $serializer->serialize($entities, 'hal_json');
-    $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/hal+json'));
+    $actual_json = $this->drupalGet('test/serialize/entity', array('query' => ['_format' => 'hal_json']));
     $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
     $this->assertCacheTags($expected_cache_tags);
   }
@@ -156,10 +156,10 @@ public function testReponseFormatConfiguration() {
     $this->drupalPostForm(NULL, array(), t('Save'));
 
     // Should return a 406.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: application/json'));
+    $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'json']));
     $this->assertResponse(406, 'A 406 response was returned when JSON was requested.');
      // Should return a 200.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: application/xml'));
+    $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'xml']));
     $this->assertResponse(200, 'A 200 response was returned when XML was requested.');
 
     // Add 'json' as an accepted format, so we have multiple.
@@ -168,7 +168,7 @@ public function testReponseFormatConfiguration() {
 
     // Should return a 200.
     // @todo This should be fixed when we have better content negotiation.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: */*'));
+    $this->drupalGet('test/serialize/field');
     $this->assertResponse(200, 'A 200 response was returned when any format was requested.');
 
     // Should return a 200. Emulates a sample Firefox header.
@@ -176,14 +176,14 @@ public function testReponseFormatConfiguration() {
     $this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.');
 
     // Should return a 200.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: application/json'));
+    $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'json']));
     $this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
     // Should return a 200.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: application/xml'));
+    $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'xml']));
     $this->assertResponse(200, 'A 200 response was returned when XML was requested');
     // Should return a 406.
-    $this->drupalGet('test/serialize/field', array(), array('Accept: application/html'));
-    $this->assertResponse(406, 'A 406 response was returned when HTML was requested.');
+    $this->drupalGet('test/serialize/field', array('query' => ['_format' => 'html']));
+    $this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
   }
 
   /**
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index fe96f9f..a8e99d6 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -13,6 +13,7 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Cache\Cache;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\DependencyInjection\YamlFileLoader;
@@ -20,6 +21,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\ConnectionNotDefinedException;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Session\AccountInterface;
@@ -1556,8 +1558,8 @@ protected function drupalGetJSON($path, array $options = array(), array $headers
    * Requests a Drupal path in drupal_ajax format and JSON-decodes the response.
    */
   protected function drupalGetAjax($path, array $options = array(), array $headers = array()) {
-    if (!preg_grep('/^Accept:/', $headers)) {
-      $headers[] = 'Accept: application/vnd.drupal-ajax';
+    if (!isset($options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE])) {
+      $options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE] = 'drupal_ajax';
     }
     return Json::decode($this->drupalGet($path, $options, $headers));
   }
@@ -1785,6 +1787,11 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array(
    * @see ajax.js
    */
   protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
+    if (isset($options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE])) {
+      $original_wrapper = $options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE];
+      unset($options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE]);
+    }
+
     // Get the content of the initial page prior to calling drupalPostForm(),
     // since drupalPostForm() replaces $this->content.
     if (isset($path)) {
@@ -1793,8 +1800,11 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
     $content = $this->content;
     $drupal_settings = $this->drupalSettings;
 
-    if (!preg_grep('/^Accept:/', $headers)) {
-      $headers[] = 'Accept: application/vnd.drupal-ajax';
+    if (isset($original_wrapper)) {
+      $options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE] = $original_wrapper;
+    }
+    else {
+      $options['query'][MainContentViewSubscriber::WRAPPER_ENVELOPE] = 'drupal_ajax';
     }
 
     // Get the Ajax settings bound to the triggering element.
@@ -1834,8 +1844,21 @@ protected function drupalPostAjaxForm($path, $edit, $triggering_element, $ajax_p
     // Unless a particular path is specified, use the one specified by the
     // Ajax settings, or else 'system/ajax'.
     if (!isset($ajax_path)) {
-      $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax';
+      if (isset($ajax_settings['url'])) {
+        $parsed_url = UrlHelper::parse($ajax_settings['url']);
+        $options['query'] = $parsed_url['query'] + $options['query'];
+        $options += ['fragment' => $parsed_url['fragment']];
+        $ajax_path = preg_replace(
+          '/^' . preg_quote($GLOBALS['base_path'], '/') . '/',
+          '',
+          $parsed_url['path']
+        );
+      }
+      else {
+        $ajax_path = 'system/ajax';
+      }
     }
+    $ajax_path = $this->container->get('unrouted_url_assembler')->assemble('base://' . $ajax_path, $options);
 
     // Submit the POST request.
     $return = Json::decode($this->drupalPostForm(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post));
diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php
index 482e025..909350f 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Ajax;
 
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Url;
 
 /**
@@ -93,7 +94,7 @@ public function testDialog() {
     $this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.');
 
     // Emulate going to the JS version of the page and check the JSON response.
-    $ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-modal'));
+    $ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_modal')));
     $this->assertEqual($modal_expected_response, $ajax_result[3], 'Modal dialog JSON response matches.');
 
     // Check that requesting a "normal" dialog without JS goes to a page.
@@ -106,7 +107,7 @@ public function testDialog() {
     $ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
         // We have to mock a form element to make drupalPost submit from a link.
         'textfield' => 'test',
-      ), array(), 'ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-dialog'), NULL, array(
+      ), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_dialog')), array(), NULL, array(
       'submit' => array(
         'dialogOptions[target]' => 'ajax-test-dialog-wrapper-1',
       )
@@ -119,7 +120,7 @@ public function testDialog() {
     $ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
         // We have to mock a form element to make drupalPost submit from a link.
         'textfield' => 'test',
-      ), array(), 'ajax-test/dialog-contents', array(), array('Accept: application/vnd.drupal-dialog'), NULL, array(
+      ), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_dialog')), array(), NULL, array(
       // Don't send a target.
       'submit' => array()
     ));
@@ -159,13 +160,13 @@ public function testDialog() {
     $this->assertTrue(!empty($form), 'Non-JS form page present.');
 
     // Emulate going to the JS version of the form and check the JSON response.
-    $ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array(), array('Accept: application/vnd.drupal-modal'));
+    $ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array('query' => array(MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_modal')));
     $expected_ajax_settings = [
       'edit-preview' => [
         'callback' => '::preview',
         'event' => 'click',
-        'url' => Url::fromRoute('system.ajax')->toString(),
-        'accepts' => 'application/vnd.drupal-ajax',
+        'url' => Url::fromRoute('system.ajax', ['_format' => NULL])->toString(),
+        MainContentViewSubscriber::WRAPPER_ENVELOPE => 'ajax',
         'submit' => [
           '_triggering_element_name' => 'op',
           '_triggering_element_value' => 'Preview',
@@ -188,7 +189,7 @@ public function testDialog() {
     $this->assertTrue(!empty($form), 'Non-JS entity form page present.');
 
     // Emulate going to the JS version of the form and check the JSON response.
-    $ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array(), array('Accept: application/vnd.drupal-modal'));
+    $ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array('query' => array(MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_modal')));
     $this->setRawContent($ajax_result[3]['data']);
     // Remove the data, the form build id and token will never match.
     unset($ajax_result[3]['data']);
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..8eb8ecb
--- /dev/null
+++ b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\conneg_test\Tests\ContentNegotiationRoutingTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+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();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function containerBuild(ContainerBuilder $container) {
+    parent::containerBuild($container);
+
+    // \Drupal\simpletest\KernelTestBase::containerBuild removes the alias path
+    // processor.
+    if ($container->hasDefinition('path_processor_alias')) {
+      $definition = $container->getDefinition('path_processor_alias');
+      $definition->addTag('path_processor_inbound', ['priority' => 100])->addTag('path_processor_outbound', ['priority' => 300]);
+    }
+  }
+
+  function testContentRouting() {
+    /** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */
+    $path_alias_storage = $this->container->get('path.alias_storage');
+    // Alias with extension pointing to no extension/constant content-type.
+    $path_alias_storage->save('conneg/html', 'alias.html');
+
+    // Alias with extension pointing to dynamic extension/linked content-type.
+    $path_alias_storage->save('conneg/html?_format=json', 'alias.json');
+
+    $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?_format=json', '', 'application/json', FALSE],
+      ['conneg/html?_format=json', '*/*', 'application/json', FALSE],
+      ['conneg/html?_format=json', 'application/xml', 'application/json', FALSE],
+      ['conneg/html?_format=json', 'application/json', 'application/json', FALSE],
+      // @TODO We have to turn of full content negotiation to support this.
+      ['conneg/html?_format=xml', '', 'application/xml', FALSE],
+      ['conneg/html?_format=xml', '*/*', 'application/xml', FALSE],
+      ['conneg/html?_format=xml', 'application/json', 'application/xml', FALSE],
+      ['conneg/html?_format=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.
+      ['alias.html', '', 'text/html', FALSE],
+      ['alias.html', '*/*', 'text/html', FALSE],
+      ['alias.html', 'text/xml', 'text/html', FALSE],
+      ['alias.html', 'text/html', 'text/html', FALSE],
+
+      // Alias with extension pointing to dynamic extension/linked content-type.
+      // @todo Path aliases with query strings aren't supported at the moment.
+//      ['alias.json', '', 'application/json', FALSE],
+//      ['alias.json', '*/*', 'application/json', FALSE],
+//      ['alias.json', 'application/xml', 'application/json', FALSE],
+//      ['alias.json', 'application/json', 'application/json', FALSE],
+
+      // 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->assertTrue(strpos($response->headers->get('Content-type'), $test[2]) !== FALSE);
+
+      if ($test[3]) {
+        $this->assertEqual($response->headers->get('Cache-Control'), 'public');
+        $this->assertEqual($response->headers->get('Vary'), 'Cookie');
+      }
+    }
+  }
+
+  /**
+   * Full negotiation by header only.
+   */
+  public function ptestFullNegotiation() {
+    $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.
+   */
+  public function ptestContentRouting400() {
+    $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/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
index b54b823..ad5adc3 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
@@ -35,7 +35,8 @@ public static function dialogContents() {
       'cancel' => array(
         '#type' => 'link',
         '#title' => 'Cancel',
-        '#url' => Url::fromRoute('<front>'),
+        // Ensure that rendered in simpletest context we don't render a format.
+        '#url' => Url::fromRoute('<front>', ['_format' => NULL]),
         '#attributes' => array(
           // This is a special class to which JavaScript assigns dialog closing
           // behavior.
@@ -120,7 +121,7 @@ public function dialog() {
       '#url' => Url::fromRoute('ajax_test.dialog_contents'),
       '#attributes' => array(
         'class' => array('use-ajax'),
-        'data-accepts' => 'application/vnd.drupal-modal',
+        'data-accepts' => 'application/vnd.drupal-modal', // TODO replace this.
       ),
     );
 
@@ -133,7 +134,7 @@ public function dialog() {
           'url' => Url::fromRoute('ajax_test.dialog_contents'),
           'attributes' => array(
             'class' => array('use-ajax'),
-            'data-accepts' => 'application/vnd.drupal-modal',
+            'data-dialog-type' => 'modal',
             'data-dialog-options' => json_encode(array(
               'width' => 400,
             ))
@@ -144,7 +145,7 @@ public function dialog() {
           'url' => Url::fromRoute('ajax_test.dialog_contents'),
           'attributes' => array(
             'class' => array('use-ajax'),
-            'data-accepts' => 'application/vnd.drupal-dialog',
+            'data-dialog-type' => 'dialog',
             'data-dialog-options' => json_encode(array(
               'target' => 'ajax-test-dialog-wrapper-1',
               'width' => 800,
@@ -163,7 +164,7 @@ public function dialog() {
           'url' => Url::fromRoute('ajax_test.dialog_form'),
           'attributes' => array(
             'class' => array('use-ajax'),
-            'data-accepts' => 'application/vnd.drupal-modal',
+            'data-dialog-type' => 'modal',
           ),
         ),
         'link6' => array(
@@ -171,7 +172,7 @@ public function dialog() {
           'url' => Url::fromRoute('contact.form_add'),
           'attributes' => array(
             'class' => array('use-ajax'),
-            'data-accepts' => 'application/vnd.drupal-modal',
+            'data-dialog-type' => 'modal',
             'data-dialog-options' => json_encode(array(
               'width' => 800,
               'height' => 500,
@@ -183,7 +184,7 @@ public function dialog() {
           'url' => Url::fromRoute('ajax_test.dialog_contents'),
           'attributes' => array(
             'class' => array('use-ajax'),
-            'data-accepts' => 'application/vnd.drupal-dialog',
+            'data-dialog-type' => 'dialog',
             'data-dialog-options' => json_encode(array(
               'width' => 800,
             ))
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.routing.yml b/core/modules/system/tests/modules/conneg_test/conneg_test.routing.yml
new file mode 100644
index 0000000..a660d09
--- /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
+  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..ccedbaa
--- /dev/null
+++ b/core/modules/system/tests/modules/conneg_test/src/Controller/Test.php
@@ -0,0 +1,46 @@
+<?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:
+        return $request->getRequestFormat();
+        // var_dump($request->getRequestFormat());
+    }
+  }
+
+  public function variable($plugin_id) {
+    return [
+      '#markup' => $plugin_id,
+    ];
+  }
+
+}
diff --git a/core/modules/views_ui/src/Tests/PreviewTest.php b/core/modules/views_ui/src/Tests/PreviewTest.php
index ff10213..e42147b 100644
--- a/core/modules/views_ui/src/Tests/PreviewTest.php
+++ b/core/modules/views_ui/src/Tests/PreviewTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\views_ui\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 
 /**
  * Tests the UI preview functionality.
@@ -263,7 +264,7 @@ protected function clickPreviewLinkAJAX($url, $row_count) {
     );
     $url = $this->getAbsoluteUrl($url);
     $post = array('js' => 'true') + $this->getAjaxPageStatePostData();
-    $result = Json::decode($this->drupalPost($url, 'application/vnd.drupal-ajax', $post));
+    $result = Json::decode($this->drupalPost($url, '', $post, [MainContentViewSubscriber::WRAPPER_ENVELOPE => 'drupal_ajax']));
     if (!empty($result)) {
       $this->drupalProcessAjaxResponse($content, $result, $ajax_settings, $drupal_settings);
     }
diff --git a/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php b/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php
index e411008..d5f3081 100644
--- a/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php
+++ b/core/tests/Drupal/Tests/Core/ContentNegotiationTest.php
@@ -60,9 +60,6 @@ public function testAPriorityFormatIsFound($priority, $format) {
   public function priorityFormatProvider()
   {
     return [
-      ['drupal_dialog', ['format' => 'drupal_dialog', 'mime_type' => 'application/vnd.drupal-dialog']],
-      ['drupal_modal', ['format' => 'drupal_modal', 'mime_type' => 'application/vnd.drupal-modal']],
-      ['drupal_ajax', ['format' => 'drupal_ajax', 'mime_type' => 'application/vnd.drupal-ajax']],
       ['html', ['format' => 'html', 'mime_type' => 'text/html']],
     ];
   }
