diff --git a/core/core.services.yml b/core/core.services.yml
index 802d143..e6ab523 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -352,8 +352,13 @@ services:
   password:
     class: Drupal\Core\Password\PhpassHashedPassword
     arguments: [16]
-  mime_type_matcher:
-    class: Drupal\Core\Routing\MimeTypeMatcher
+  accept_header_matcher:
+    class: Drupal\Core\Routing\AcceptHeaderMatcher
+    arguments: ['@content_negotiation']
+    tags:
+      - { name: route_filter }
+  content_type_header_matcher:
+    class: Drupal\Core\Routing\ContentTypeHeaderMatcher
     tags:
       - { name: route_filter }
   paramconverter_manager:
@@ -427,6 +432,10 @@ services:
     class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
     tags:
       - { name: event_subscriber }
+  route_http_method_subscriber:
+    class: Drupal\Core\EventSubscriber\RouteMethodSubscriber
+    tags:
+      - { name: event_subscriber }
   controller.page:
     class: Drupal\Core\Controller\HtmlPageController
     arguments: ['@controller_resolver', '@string_translation', '@title_resolver']
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php
new file mode 100644
index 0000000..f422b2a
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\RouteMethodSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Provides a default value for the HTTP method restriction on routes.
+ *
+ * Most routes will only deal with GET and POST requests, so we restrict them to
+ * those two if nothing else is specified. This is necessary to give other
+ * routes a chance during the route matching process when they are listening
+ * for example to DELETE requests on the same path. A typical use case are REST
+ * web service routes that use the full spectrum of HTTP methods.
+ */
+class RouteMethodSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Sets a default value of GET|POST for the _method route property.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The event containing the build routes.
+   */
+  public function onRouteBuilding(RouteBuildEvent $event) {
+    foreach ($event->getRouteCollection() as $route) {
+      $methods = $route->getMethods();
+      if (empty($methods)) {
+        $route->setMethods(array('GET', 'POST'));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    // Set a higher priority to ensure that routes get the default HTTP methods
+    // as early as possible.
+    $events[RoutingEvents::ALTER][] = array('onRouteBuilding', 5000);
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
new file mode 100644
index 0000000..34f0ae5
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Routing\AcceptHeaderMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\ContentNegotiation;
+use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Filters routes based on the media type specified in the HTTP Accept headers.
+ */
+class AcceptHeaderMatcher implements RouteFilterInterface {
+
+  /**
+   * The content negotiation library.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $contentNegotiation;
+
+  /**
+   * Constructs a new AcceptHeaderMatcher.
+   *
+   * @param \Drupal\Core\ContentNegotiation $cotent_negotiation
+   *   The content negotiation library.
+   */
+  public function __construct(ContentNegotiation $content_negotiation) {
+    $this->contentNegotiation = $content_negotiation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(RouteCollection $collection, Request $request) {
+    // Generates a list of Symfony formats matching the acceptable MIME types.
+    // @todo replace by proper content negotiation library.
+    $acceptable_mime_types = $request->getAcceptableContentTypes();
+    $acceptable_formats = array_filter(array_map(array($request, 'getFormat'), $acceptable_mime_types));
+    $primary_format = $this->contentNegotiation->getContentType($request);
+
+    foreach ($collection as $name => $route) {
+      // _format could be a |-delimited list of supported formats.
+      $supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
+
+      if (empty($supported_formats)) {
+        // No format restriction on the route, so it always matches. Move it to
+        // the end of the collection by re-adding it.
+        $collection->add($name, $route);
+      }
+      elseif (in_array($primary_format, $supported_formats)) {
+        // Perfect match, which will get a higher priority by leaving the route
+        // on top of the list.
+      }
+      // The route partially matches if it doesn't care about format, if it
+      // explicitly allows any format, or if one of its allowed formats is
+      // in the request's list of acceptable formats.
+      elseif (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
+        // Move it to the end of the list.
+        $collection->add($name, $route);
+      }
+      else {
+        // Remove the route if it does not match at all.
+        $collection->remove($name);
+      }
+    }
+
+    if (count($collection)) {
+      return $collection;
+    }
+
+    // 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(String::format('No route found for the specified formats @formats.', array('@formats' => implode(' ', $acceptable_mime_types))));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php
new file mode 100644
index 0000000..c76036d
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Routing\ContentTypeHeaderMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Filters routes based on the HTTP Content-type header.
+ */
+class ContentTypeHeaderMatcher implements RouteFilterInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(RouteCollection $collection, Request $request) {
+    // The Content-type header does not make sense on GET requests, because GET
+    // requests do not carry any content. Nothing to filter in this case.
+    if ($request->isMethod('GET')) {
+      return $collection;
+    }
+
+    $format = $request->getContentType();
+
+    foreach ($collection as $name => $route) {
+      $supported_formats = array_filter(explode('|', $route->getRequirement('_content_type_format')));
+      if (empty($supported_formats)) {
+        // No restriction on the route, so we move the route to the end of the
+        // collection by re-adding it. That way generic routes sink down in the
+        // list and exact matching routes stay on top.
+        $collection->add($name, $route);
+      }
+      elseif (!in_array($format, $supported_formats)) {
+        $collection->remove($name);
+      }
+    }
+    if (count($collection)) {
+      return $collection;
+    }
+    // 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 415.
+    throw new UnsupportedMediaTypeHttpException('No route found that matches the Content-Type header.');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php b/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php
deleted file mode 100644
index f77e254..0000000
--- a/core/lib/Drupal/Core/Routing/MimeTypeMatcher.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\Core\Routing\MimeTypeMatcher.
- */
-
-namespace Drupal\Core\Routing;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
-use Symfony\Component\Routing\RouteCollection;
-use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
-
-/**
- * This class filters routes based on the media type in HTTP Accept headers.
- */
-class MimeTypeMatcher implements RouteFilterInterface {
-
-
-  /**
-   * Implements \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface::filter()
-   */
-  public function filter(RouteCollection $collection, Request $request) {
-    // Generates a list of Symfony formats matching the acceptable MIME types.
-    // @todo replace by proper content negotiation library.
-    $acceptable_mime_types = $request->getAcceptableContentTypes();
-    $acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types);
-
-    $filtered_collection = new RouteCollection();
-
-    foreach ($collection as $name => $route) {
-      // _format could be a |-delimited list of supported formats.
-      $supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
-      // The route partially matches if it doesn't care about format, if it
-      // explicitly allows any format, or if one of its allowed formats is
-      // in the request's list of acceptable formats.
-      if (empty($supported_formats) || in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
-        $filtered_collection->add($name, $route);
-      }
-    }
-
-    if (!count($filtered_collection)) {
-      throw new NotAcceptableHttpException();
-    }
-
-    return $filtered_collection;
-  }
-
-}
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
index f77d5e3..c34f4e7 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
@@ -9,7 +9,9 @@
 
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
 
 /**
  * Provides a resource plugin definition for every entity type.
@@ -31,13 +33,23 @@ class EntityDerivative implements ContainerDerivativeInterface {
   protected $entityManager;
 
   /**
+   * The route provider.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
    * Constructs an EntityDerivative object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider.
    */
-  public function __construct(EntityManagerInterface $entity_manager) {
+  public function __construct(EntityManagerInterface $entity_manager, RouteProviderInterface $route_provider) {
     $this->entityManager = $entity_manager;
+    $this->routeProvider = $route_provider;
   }
 
   /**
@@ -45,7 +57,8 @@ public function __construct(EntityManagerInterface $entity_manager) {
    */
   public static function create(ContainerInterface $container, $base_plugin_id) {
     return new static(
-      $container->get('entity.manager')
+      $container->get('entity.manager'),
+      $container->get('router.route_provider')
     );
   }
 
@@ -74,6 +87,34 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           'serialization_class' => $entity_type->getClass(),
           'label' => $entity_type->getLabel(),
         );
+
+        $default_uris = array(
+          'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
+          'http://drupal.org/link-relations/create' => "/entity/$entity_type_id",
+        );
+
+        foreach ($default_uris as $link_relation => $default_uri) {
+          // Check if there are link templates defined for the entity type and
+          // use the path from the route instead of the default.
+          if ($route_name = $entity_type->getLinkTemplate($link_relation)) {
+            try {
+              $route = $this->routeProvider->getRouteByName($route_name);
+              $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $route->getPath();
+            }
+            catch (RouteNotFoundException $e) {
+              // If the route does not exist it means we are in a brittle state
+              // of module enabling/disabling, so we simply exclude this entity
+              // type.
+              unset($this->derivatives[$entity_type_id]);
+              // Continue with the next entity type;
+              continue 2;
+            }
+          }
+          else {
+            $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $default_uri;
+          }
+        }
+
         $this->derivatives[$entity_type_id] += $base_plugin_definition;
       }
     }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
index 9100f45..6d59e54 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -78,13 +78,17 @@ public function permissions() {
    */
   public function routes() {
     $collection = new RouteCollection();
-    $path_prefix = strtr($this->pluginId, ':', '/');
+
+    $definition = $this->getPluginDefinition();
+    $canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
+    $create_path = isset($definition['uri_paths']['http://drupal.org/link-relations/create']) ? $definition['uri_paths']['http://drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');
+
     $route_name = strtr($this->pluginId, ':', '.');
 
     $methods = $this->availableMethods();
     foreach ($methods as $method) {
       $lower_method = strtolower($method);
-      $route = new Route("/$path_prefix/{id}", array(
+      $route = new Route($canonical_path, array(
         '_controller' => 'Drupal\rest\RequestHandler::handle',
         // Pass the resource plugin ID along as default property.
         '_plugin' => $this->pluginId,
@@ -98,9 +102,17 @@ public function routes() {
 
       switch ($method) {
         case 'POST':
-          // POST routes do not require an ID in the URL path.
-          $route->setPattern("/$path_prefix");
-          $route->addDefaults(array('id' => NULL));
+          $route->setPattern($create_path);
+          // Restrict the incoming HTTP Content-type header to the known
+          // serialization formats.
+          $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
+          $collection->add("$route_name.$method", $route);
+          break;
+
+        case 'PATCH':
+          // Restrict the incoming HTTP Content-type header to the known
+          // serialization formats.
+          $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
           $collection->add("$route_name.$method", $route);
           break;
 
@@ -110,7 +122,6 @@ public function routes() {
           // HTTP Accept headers.
           foreach ($this->serializerFormats as $format_name) {
             // Expose one route per available format.
-            //$format_route = new Route($route->getPath(), $route->getDefaults(), $route->getRequirements());
             $format_route = clone $route;
             $format_route->addRequirements(array('_format' => $format_name));
             $collection->add("$route_name.$method.$format_name", $format_route);
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
index 57aaf81..4612222 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -17,7 +17,10 @@
  *
  * @RestResource(
  *   id = "dblog",
- *   label = @Translation("Watchdog database log")
+ *   label = @Translation("Watchdog database log"),
+ *   uri_paths = {
+ *     "canonical" = "/dblog/{id}"
+ *   }
  * )
  */
 class DBLogResource extends ResourceBase {
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
index 6cbb282..9b3c792 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
@@ -14,7 +14,6 @@
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Represents entities as resources.
@@ -23,7 +22,11 @@
  *   id = "entity",
  *   label = @Translation("Entity"),
  *   serialization_class = "Drupal\Core\Entity\Entity",
- *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
+ *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative",
+ *   uri_paths = {
+ *     "canonical" = "/entity/{entity_type}/{entity}",
+ *     "http://drupal.org/link-relations/create" = "/entity/{entity_type}"
+ *   }
  * )
  */
 class EntityResource extends ResourceBase {
@@ -31,36 +34,29 @@ class EntityResource extends ResourceBase {
   /**
    * Responds to entity GET requests.
    *
-   * @param mixed $id
-   *   The entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity object.
    *
    * @return \Drupal\rest\ResourceResponse
-   *   The response containing the loaded entity.
+   *   The response containing the entity with its accessible fields.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function get($id) {
-    $definition = $this->getPluginDefinition();
-    $entity = entity_load($definition['entity_type'], $id);
-    if ($entity) {
-      if (!$entity->access('view')) {
-        throw new AccessDeniedHttpException();
-      }
-      foreach ($entity as $field_name => $field) {
-        if (!$field->access('view')) {
-          unset($entity->{$field_name});
-        }
+  public function get(EntityInterface $entity) {
+    if (!$entity->access('view')) {
+      throw new AccessDeniedHttpException();
+    }
+    foreach ($entity as $field_name => $field) {
+      if (!$field->access('view')) {
+        unset($entity->{$field_name});
       }
-      return new ResourceResponse($entity);
     }
-    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
+    return new ResourceResponse($entity);
   }
 
   /**
    * Responds to entity POST requests and saves the new entity.
    *
-   * @param mixed $id
-   *   Ignored. A new entity is created with a new ID.
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity.
    *
@@ -69,7 +65,7 @@ public function get($id) {
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function post($id, EntityInterface $entity = NULL) {
+  public function post(EntityInterface $entity = NULL) {
     if ($entity == NULL) {
       throw new BadRequestHttpException(t('No entity content received.'));
     }
@@ -112,8 +108,8 @@ public function post($id, EntityInterface $entity = NULL) {
   /**
    * Responds to entity PATCH requests.
    *
-   * @param mixed $id
-   *   The entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $original_entity
+   *   The original entity object.
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity.
    *
@@ -122,24 +118,14 @@ public function post($id, EntityInterface $entity = NULL) {
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function patch($id, EntityInterface $entity = NULL) {
+  public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
     if ($entity == NULL) {
       throw new BadRequestHttpException(t('No entity content received.'));
     }
-
-    if (empty($id)) {
-      throw new NotFoundHttpException();
-    }
     $definition = $this->getPluginDefinition();
     if ($entity->getEntityTypeId() != $definition['entity_type']) {
       throw new BadRequestHttpException(t('Invalid entity type'));
     }
-    $original_entity = entity_load($definition['entity_type'], $id);
-    // We don't support creating entities with PATCH, so we throw an error if
-    // there is no existing entity.
-    if ($original_entity == FALSE) {
-      throw new NotFoundHttpException();
-    }
     if (!$original_entity->access('update')) {
       throw new AccessDeniedHttpException();
     }
@@ -174,33 +160,28 @@ public function patch($id, EntityInterface $entity = NULL) {
   /**
    * Responds to entity DELETE requests.
    *
-   * @param mixed $id
-   *   The entity ID.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity object.
    *
    * @return \Drupal\rest\ResourceResponse
    *   The HTTP response object.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function delete($id) {
-    $definition = $this->getPluginDefinition();
-    $entity = entity_load($definition['entity_type'], $id);
-    if ($entity) {
-      if (!$entity->access('delete')) {
-        throw new AccessDeniedHttpException();
-      }
-      try {
-        $entity->delete();
-        watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
+  public function delete(EntityInterface $entity) {
+    if (!$entity->access('delete')) {
+      throw new AccessDeniedHttpException();
+    }
+    try {
+      $entity->delete();
+      watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
 
-        // Delete responses have an empty body.
-        return new ResourceResponse(NULL, 204);
-      }
-      catch (EntityStorageException $e) {
-        throw new HttpException(500, t('Internal Server Error'), $e);
-      }
+      // Delete responses have an empty body.
+      return new ResourceResponse(NULL, 204);
+    }
+    catch (EntityStorageException $e) {
+      throw new HttpException(500, t('Internal Server Error'), $e);
     }
-    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
   }
 
   /**
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index 605a1f8..120ccfa 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -25,13 +25,12 @@ class RequestHandler extends ContainerAware {
    *
    * @param Symfony\Component\HttpFoundation\Request $request
    *   The HTTP request object.
-   * @param mixed $id
-   *   The resource ID.
    *
    * @return \Symfony\Component\HttpFoundation\Response
    *   The response object.
    */
-  public function handle(Request $request, $id = NULL) {
+  public function handle(Request $request) {
+
     $plugin = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getDefault('_plugin');
     $method = strtolower($request->getMethod());
 
@@ -69,13 +68,24 @@ public function handle(Request $request, $id = NULL) {
       }
     }
 
+    // Determine the request parameters that should be passed to the resource
+    // plugin.
+    $route_parameters = $request->attributes->get('_route_params');
+    $parameters = array();
+    // Filter out all internal parameters starting with "_".
+    foreach ($route_parameters as $key => $parameter) {
+      if ($key{0} !== '_') {
+        $parameters[] = $parameter;
+      }
+    }
+
     // Invoke the operation on the resource plugin.
     // All REST routes are restricted to exactly one format, so instead of
     // parsing it out of the Accept headers again, we can simply retrieve the
     // format requirement. If there is no format associated, just pick JSON.
     $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'json';
     try {
-      $response = $resource->{$method}($id, $unserialized, $request);
+      $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
     }
     catch (HttpException $e) {
       $error['error'] = $e->getMessage();
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
index 41d4d48..3add3aa 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
@@ -46,7 +46,7 @@ public function testRead() {
     $entity->save();
 
     // Try to read the resource as an anonymous user, which should not work.
-    $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
     $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.');
     $this->assertText('A fatal error occurred: No authentication credentials provided.');
 
@@ -63,7 +63,7 @@ public function testRead() {
 
     // Try to read the resource with session cookie authentication, which is
     // not enabled and should not work.
-    $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
     $this->assertResponse('401', 'HTTP response code is 401 when the request is authenticated but not authorized.');
 
     // Ensure that cURL settings/headers aren't carried over to next request.
@@ -71,7 +71,7 @@ public function testRead() {
 
     // Now read it with the Basic authentication which is enabled and should
     // work.
-    $response = $this->basicAuthGet('entity/' . $entity_type . '/' . $entity->id(), $account->getUsername(), $account->pass_raw);
+    $this->basicAuthGet($entity->getSystemPath(), $account->getUsername(), $account->pass_raw);
     $this->assertResponse('200', 'HTTP response code is 200 for successfully authorized requests.');
     $this->curlClose();
   }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index aec525d..799862e 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\rest\Tests;
 
-use Drupal\Component\Utility\Json;
 use Drupal\rest\Tests\RESTTestBase;
 
 /**
@@ -51,7 +50,7 @@ public function testDelete() {
       $entity = $this->entityCreate($entity_type);
       $entity->save();
       // Delete it over the REST API.
-      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE');
+      $response = $this->httpRequest($entity->getSystemPath(), 'DELETE');
       // Clear the static cache with entity_load(), otherwise we won't see the
       // update.
       $entity = entity_load($entity_type, $entity->id(), TRUE);
@@ -60,17 +59,16 @@ public function testDelete() {
       $this->assertEqual($response, '', 'Response body is empty.');
 
       // Try to delete an entity that does not exist.
-      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'DELETE');
+      $response = $this->httpRequest($entity_type . '/9999', 'DELETE');
       $this->assertResponse(404);
-      $decoded = Json::decode($response);
-      $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
+      $this->assertText('The requested page "/' . $entity_type . '/9999" could not be found.');
 
       // Try to delete an entity without proper permissions.
       $this->drupalLogout();
       // Re-save entity to the database.
       $entity = $this->entityCreate($entity_type);
       $entity->save();
-      $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE');
+      $this->httpRequest($entity->getSystemPath(), 'DELETE');
       $this->assertResponse(403);
       $this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
     }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php
index c4a1704..a85774b 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php
@@ -55,8 +55,16 @@ public function testNodes() {
 
     $node = $this->entityCreate('node');
     $node->save();
-    $this->httpRequest('entity/node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
+    $this->httpRequest('node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
     $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/' . $node->id(), 'GET', NULL, 'application/json');
+    $this->assertResponse(200);
+    $this->assertHeader('Content-type', 'application/json');
 
     // Check that a simple PATCH update to the node title works as expected.
     $this->enableNodeConfiguration('PATCH', 'update');
@@ -76,7 +84,7 @@ public function testNodes() {
       ),
     );
     $serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat);
-    $this->httpRequest('entity/node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest('node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(204);
 
     // Reload the node from the DB and check if the title was correctly updated.
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index 5f3ed46..fdf56d9 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -276,16 +276,22 @@ protected function assertHeader($header, $value, $message = '', $group = 'Browse
   }
 
   /**
-   * Overrides WebTestBase::drupalLogin().
+   * {@inheritdoc}
+   *
+   * This method is overridden to deal with a cURL quirk: the usage of
+   * CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to
+   * override it every time it is omitted.
    */
-  protected function drupalLogin(AccountInterface $user) {
-    if (isset($this->curlHandle)) {
-      // cURL quirk: when setting CURLOPT_CUSTOMREQUEST to anything other than
-      // POST in httpRequest() it has to be restored to POST here. Otherwise the
-      // POST request to login a user will not work.
-      curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
+  protected function curlExec($curl_options, $redirect = FALSE) {
+    if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) {
+      if (!empty($curl_options[CURLOPT_HTTPGET])) {
+        $curl_options[CURLOPT_CUSTOMREQUEST] = 'GET';
+      }
+      if (!empty($curl_options[CURLOPT_POST])) {
+        $curl_options[CURLOPT_CUSTOMREQUEST] = 'POST';
+      }
     }
-    parent::drupalLogin($user);
+    return parent::curlExec($curl_options, $redirect);
   }
 
   /**
@@ -338,4 +344,5 @@ protected function loadEntityFromLocationHeader($location_url) {
     $id = end($url_parts);
     return entity_load($this->testEntityType, $id);
   }
+
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index 7ddde9d..44c5e15 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -51,7 +51,7 @@ public function testRead() {
       $entity = $this->entityCreate($entity_type);
       $entity->save();
       // Read it over the REST API.
-      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse('200', 'HTTP response code is correct.');
       $this->assertHeader('content-type', $this->defaultMimeType);
       $data = Json::decode($response);
@@ -60,14 +60,14 @@ 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.
-      $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
-      $this->assertResponse(406);
+      $response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, 'application/wrongformat');
+      $this->assertResponse(200);
+      $this->assertHeader('Content-type', 'text/html; charset=UTF-8');
 
       // Try to read an entity that does not exist.
-      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest($entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse(404);
-      $decoded = Json::decode($response);
-      $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
+      $this->assertText('A fatal error occurred: The "' . $entity_type . '" parameter was not converted for the path', 'Response message is correct.');
 
       // Make sure that field level access works and that the according field is
       // not available in the response. Only applies to entity_test.
@@ -75,7 +75,7 @@ public function testRead() {
       if ($entity_type == 'entity_test') {
         $entity->field_test_text->value = 'no access value';
         $entity->save();
-        $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
+        $response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
         $this->assertResponse(200);
         $this->assertHeader('content-type', $this->defaultMimeType);
         $data = Json::decode($response);
@@ -84,14 +84,14 @@ public function testRead() {
 
       // Try to read an entity without proper permissions.
       $this->drupalLogout();
-      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
+      $response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse(403);
       $this->assertNull(Json::decode($response), 'No valid JSON found.');
     }
     // Try to read a resource which is not REST API enabled.
     $account = $this->drupalCreateUser();
     $this->drupalLogin($account);
-    $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, $this->defaultMimeType);
+    $response = $this->httpRequest($account->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
     $this->assertResponse(404);
     $this->assertNull(Json::decode($response), 'No valid JSON found.');
   }
@@ -114,7 +114,7 @@ public function testResourceStructure() {
     $entity->save();
 
     // Read it over the REST API.
-    $response = $this->httpRequest('entity/node/' . $entity->id(), 'GET', NULL, 'application/json');
+    $response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, 'application/json');
     $this->assertResponse('200', 'HTTP response code is correct.');
   }
 
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ResourceTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ResourceTest.php
index 0afd0ad..92d2074 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ResourceTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ResourceTest.php
@@ -64,7 +64,7 @@ public function testFormats() {
     $this->rebuildCache();
 
     // Verify that accessing the resource returns 401.
-    $response = $this->httpRequest('entity/entity_test/' . $this->entity->id(), 'GET', NULL, $this->defaultMimeType);
+    $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
     $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define formats.');
     $this->curlClose();
   }
@@ -89,7 +89,7 @@ public function testAuthentication() {
     $this->rebuildCache();
 
     // Verify that accessing the resource returns 401.
-    $response = $this->httpRequest('entity/entity_test/' . $this->entity->id(), 'GET', NULL, $this->defaultMimeType);
+    $response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
     $this->assertResponse('404', 'HTTP response code is 404 when the resource does not define authentication.');
     $this->curlClose();
   }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
index 3bcb6ba..adff21d 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -62,7 +62,7 @@ public function testPatchUpdate() {
     $serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
 
     // Update the entity over the REST API.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(204);
 
     // Re-load updated entity from the database.
@@ -74,7 +74,7 @@ public function testPatchUpdate() {
     $normalized = $serializer->normalize($patch_entity, $this->defaultFormat);
     unset($normalized['field_test_text']);
     $serialized = $serializer->encode($normalized, $this->defaultFormat);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(204);
 
     $entity = entity_load($entity_type, $entity->id(), TRUE);
@@ -85,7 +85,7 @@ public function testPatchUpdate() {
     $serialized = $serializer->encode($normalized, $this->defaultFormat);
 
     // Update the entity over the REST API.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(204);
 
     // Re-load updated entity from the database.
@@ -99,7 +99,7 @@ public function testPatchUpdate() {
     $entity->save();
 
     // Try to empty a field that is access protected.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(403);
 
     // Re-load the entity from the database.
@@ -109,7 +109,7 @@ public function testPatchUpdate() {
     // Try to update an access protected field.
     $patch_entity->get('field_test_text')->value = 'no access value';
     $serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(403);
 
     // Re-load the entity from the database.
@@ -122,7 +122,7 @@ public function testPatchUpdate() {
       'format' => 'full_html',
     ));
     $serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(422);
 
     // Re-load the entity from the database.
@@ -134,11 +134,11 @@ public function testPatchUpdate() {
     $entity->save();
 
     // Try to send no data at all, which does not make sense on PATCH requests.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', NULL, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', NULL, $this->defaultMimeType);
     $this->assertResponse(400);
 
     // Try to update a non-existing entity with ID 9999.
-    $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(404);
     $loaded_entity = entity_load($entity_type, 9999, TRUE);
     $this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
@@ -147,21 +147,21 @@ public function testPatchUpdate() {
     // Send a UUID that is too long.
     $entity->set('uuid', $this->randomName(129));
     $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat);
-    $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
+    $response = $this->httpRequest($entity->getSystemPath(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
     $this->assertResponse(422);
     $error = Json::decode($response);
     $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
 
     // Try to update an entity without proper permissions.
     $this->drupalLogout();
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(403);
 
     // Try to update a resource which is not REST API enabled.
     $this->enableService(FALSE);
     $this->drupalLogin($account);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
-    $this->assertResponse(404);
+    $this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
+    $this->assertResponse(405);
   }
 
 }
diff --git a/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php
index 091ec9c..18262f2 100644
--- a/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php
+++ b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php
@@ -180,6 +180,9 @@ protected function getRequestForPath($path, array $exclude) {
     // @todo Use the RequestHelper once https://drupal.org/node/2090293 is
     //   fixed.
     $request = Request::create($this->request->getBaseUrl() . '/' . $path);
+    // Performance optimization: set a short accept header to reduce overhead in
+    // AcceptHeaderMatcher when matching the request.
+    $request->headers->set('Accept', 'text/html');
     // Find the system path by resolving aliases, language prefix, etc.
     $processed = $this->pathProcessor->processInbound($path, $request);
     if (empty($processed) || !empty($exclude[$processed])) {
diff --git a/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php b/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php
new file mode 100644
index 0000000..7bf2a28
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest.
+ */
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Routing\AcceptHeaderMatcher;
+use Drupal\Tests\Core\Routing\RoutingFixtures;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Basic tests for the AcceptHeaderMatcher class.
+ *
+ * @coversClassDefault \Drupal\Core\Routing\AcceptHeaderMatcher
+ */
+class AcceptHeaderMatcherTest extends UnitTestCase {
+
+  /**
+   * A collection of shared fixture data for tests.
+   *
+   * @var RoutingFixtures
+   */
+  protected $fixtures;
+
+  /**
+   * The matcher object that is going to be tested.
+   *
+   * @var \Drupal\Core\Routing\AcceptHeaderMatcher
+   */
+  protected $matcher;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Partial matcher MIME types tests',
+      'description' => 'Confirm that the mime types partial matcher is functioning properly.',
+      'group' => 'Routing',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixtures = new RoutingFixtures();
+    $this->matcher = new AcceptHeaderMatcher(new ContentNegotiation());
+  }
+
+  /**
+   * Provides data for the Accept header filtering test.
+   *
+   * @see Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest::testAcceptFiltering()
+   */
+  public function acceptFilterProvider() {
+    return array(
+      // Check that JSON routes get filtered and prioritized correctly.
+      array('application/json, text/xml;q=0.9', 'route_c', 'route_e'),
+      // Tests a JSON request with alternative JSON MIME type Accept header.
+      array('application/x-json, text/xml;q=0.9', 'route_c', 'route_e'),
+      // Tests a standard HTML request.
+      array('text/html, text/xml;q=0.9', 'route_e', 'route_c'),
+    );
+  }
+
+  /**
+   * Tests that requests using Accept headers get filtered correctly.
+   *
+   * @param string $accept_header
+   *   The HTTP Accept header value of the request.
+   * @param string $included_route
+   *   The route name that should survive the filter and be ranked first.
+   * @param string $excluded_route
+   *   The route name that should be filtered out during matching.
+   *
+   * @dataProvider acceptFilterProvider
+   */
+  public function testAcceptFiltering($accept_header, $included_route, $excluded_route) {
+    $collection = $this->fixtures->sampleRouteCollection();
+
+    $request = Request::create('path/two', 'GET');
+    $request->headers->set('Accept', $accept_header);
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 4, 'The correct number of routes was found.');
+    $this->assertNotNull($routes->get($included_route), "Route $included_route was found when matching $accept_header.");
+    $this->assertNull($routes->get($excluded_route), "Route $excluded_route was not found when matching $accept_header.");
+    foreach ($routes as $name => $route) {
+      $this->assertEquals($name, $included_route, "Route $included_route is the first one in the collection when matching $accept_header.");
+      break;
+    }
+  }
+
+  /**
+   * Confirms that the AcceptHeaderMatcher throws an exception for no-route.
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
+   * @expectedExceptionMessage No route found for the specified formats application/json text/xml.
+   */
+  public function testNoRouteFound() {
+    // Remove the sample routes that would match any method.
+    $routes = $this->fixtures->sampleRouteCollection();
+    $routes->remove('route_a');
+    $routes->remove('route_b');
+    $routes->remove('route_c');
+    $routes->remove('route_d');
+
+    $request = Request::create('path/two', 'GET');
+    $request->headers->set('Accept', 'application/json, text/xml;q=0.9');
+    $this->matcher->filter($routes, $request);
+    $this->fail('No exception was thrown.');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php b/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php
new file mode 100644
index 0000000..71b9c6b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Routing\ContentTypeHeaderMatcherTest.
+ */
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\Routing\ContentTypeHeaderMatcher;
+use Drupal\Tests\Core\Routing\RoutingFixtures;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Basic tests for the ContentTypeHeaderMatcher class.
+ *
+ * @coversClassDefault \Drupal\Core\Routing\ContentTypeHeaderMatcher
+ */
+class ContentTypeHeaderMatcherTest extends UnitTestCase {
+
+  /**
+   * A collection of shared fixture data for tests.
+   *
+   * @var RoutingFixtures
+   */
+  protected $fixtures;
+
+  /**
+   * The matcher object that is going to be tested.
+   *
+   * @var \Drupal\Core\Routing\ContentTypeHeaderMatcher
+   */
+  protected $matcher;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Content Type header matcher test',
+      'description' => 'Confirm that the content types partial matcher is functioning properly.',
+      'group' => 'Routing',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixtures = new RoutingFixtures();
+    $this->matcher = new ContentTypeHeaderMatcher();
+  }
+
+  /**
+   * Tests that routes are not filtered on GET requests.
+   */
+  public function testGetRequestFilter() {
+    $collection = $this->fixtures->sampleRouteCollection();
+    $collection->addCollection($this->fixtures->contentRouteCollection());
+
+    $request = Request::create('path/two', 'GET');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 7, 'The correct number of routes was found.');
+  }
+
+  /**
+   * Tests that XML-restricted routes get filtered out on JSON requests.
+   */
+  public function testJsonRequest() {
+    $collection = $this->fixtures->sampleRouteCollection();
+    $collection->addCollection($this->fixtures->contentRouteCollection());
+
+    $request = Request::create('path/two', 'POST');
+    $request->headers->set('Content-type', 'application/json');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 6, 'The correct number of routes was found.');
+    $this->assertNotNull($routes->get('route_f'), 'The json route was found.');
+    $this->assertNull($routes->get('route_g'), 'The xml route was not found.');
+    foreach ($routes as $name => $route) {
+      $this->assertEquals($name, 'route_f', 'The json route is the first one in the collection.');
+      break;
+    }
+  }
+
+  /**
+   * Tests route filtering on POST form submission requests.
+   */
+  public function testPostForm() {
+    $collection = $this->fixtures->sampleRouteCollection();
+    $collection->addCollection($this->fixtures->contentRouteCollection());
+
+    // Test that all XML and JSON restricted routes get filtered out on a POST
+    // form submission.
+    $request = Request::create('path/two', 'POST');
+    $request->headers->set('Content-type', 'application/www-form-urlencoded');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 5, 'The correct number of routes was found.');
+    $this->assertNull($routes->get('route_f'), 'The json route was found.');
+    $this->assertNull($routes->get('route_g'), 'The xml route was not found.');
+  }
+
+  /**
+   * Confirms that the matcher throws an exception for no-route.
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException
+   * @expectedExceptionMessage No route found that matches the Content-Type header.
+   */
+  public function testNoRouteFound() {
+    $matcher = new ContentTypeHeaderMatcher();
+
+    $routes = $this->fixtures->contentRouteCollection();
+    $request = Request::create('path/two', 'POST');
+    $request->headers->set('Content-type', 'application/hal+json');
+    $matcher->filter($routes, $request);
+    $this->fail('No exception was thrown.');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Routing/MimeTypeMatcherTest.php b/core/tests/Drupal/Tests/Core/Routing/MimeTypeMatcherTest.php
deleted file mode 100644
index b407fe8..0000000
--- a/core/tests/Drupal/Tests/Core/Routing/MimeTypeMatcherTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\Tests\Core\Routing.
- */
-
-namespace Drupal\Tests\Core\Routing;
-
-use Drupal\Core\Routing\MimeTypeMatcher;
-use Drupal\Tests\UnitTestCase;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
-
-/**
- * Basic tests for the MimeTypeMatcher class.
- *
- * @group Drupal
- * @group Routing
- */
-class MimeTypeMatcherTest extends UnitTestCase {
-
-  /**
-   * A collection of shared fixture data for tests.
-   *
-   * @var RoutingFixtures
-   */
-  protected $fixtures;
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Partial matcher MIME types tests',
-      'description' => 'Confirm that the mime types partial matcher is functioning properly.',
-      'group' => 'Routing',
-    );
-  }
-
-  public function setUp() {
-    $this->fixtures = new RoutingFixtures();
-  }
-
-  /**
-   * Confirms that the MimeType matcher matches properly.
-   *
-   * @param string $accept_header
-   *   The 'Accept` header to test.
-   * @param integer $routes_count
-   *   The number of expected routes.
-   * @param string $null_route
-   *   The route that is expected to be null.
-   * @param string $not_null_route
-   *   The route that is expected to not be null.
-   *
-   * @dataProvider providerTestFilterRoutes
-   */
-  public function testFilterRoutes($accept_header, $routes_count, $null_route, $not_null_route) {
-
-    $matcher = new MimeTypeMatcher();
-    $collection = $this->fixtures->sampleRouteCollection();
-
-    // Tests basic JSON request.
-    $request = Request::create('path/two', 'GET');
-    $request->headers->set('Accept', $accept_header);
-    $routes = $matcher->filter($collection, $request);
-    $this->assertEquals($routes_count, count($routes), 'An incorrect number of routes was found.');
-    $this->assertNull($routes->get($null_route), 'A route was found where it should be null.');
-    $this->assertNotNull($routes->get($not_null_route), 'The expected route was not found.');
-  }
-
-  /**
-   * Provides test routes for testFilterRoutes.
-   *
-   * @return array
-   *   An array of arrays, each containing the parameters necessary for the
-   *   testFilterRoutes method.
-   */
-  public function providerTestFilterRoutes() {
-    return array(
-      // Tests basic JSON request.
-      array('application/json, text/xml;q=0.9', 4, 'route_e', 'route_c'),
-
-      // Tests JSON request with alternative JSON MIME type Accept header.
-      array('application/x-json, text/xml;q=0.9', 4, 'route_e', 'route_c'),
-
-      // Tests basic HTML request.
-      array('text/html, text/xml;q=0.9', 4, 'route_c', 'route_e'),
-    );
-  }
-
-  /**
-   * Confirms that the MimeTypeMatcher matcher throws an exception for no-route.
-   *
-   * @expectedException Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
-   */
-  public function testNoRouteFound() {
-    $matcher = new MimeTypeMatcher();
-
-    // Remove the sample routes that would match any method.
-    $routes = $this->fixtures->sampleRouteCollection();
-    $routes->remove('route_a');
-    $routes->remove('route_b');
-    $routes->remove('route_c');
-    $routes->remove('route_d');
-
-    // This should throw NotAcceptableHttpException.
-    $request = Request::create('path/two', 'GET');
-    $request->headers->set('Accept', 'application/json, text/xml;q=0.9');
-    $routes = $matcher->filter($routes, $request);
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php
index 94a36db..48071ae 100644
--- a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php
+++ b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php
@@ -144,6 +144,26 @@ public function complexRouteCollection() {
   }
 
   /**
+   * Returns a Content-type restricted set of routes for testing.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   */
+  public function contentRouteCollection() {
+    $collection = new RouteCollection();
+
+    $route = new Route('path/three');
+    $route->setRequirement('_method', 'POST');
+    $route->setRequirement('_content_type_format', 'json');
+    $collection->add('route_f', $route);
+
+    $route = new Route('path/three');
+    $route->setRequirement('_method', 'PATCH');
+    $route->setRequirement('_content_type_format', 'xml');
+    $collection->add('route_g', $route);
+    return $collection;
+  }
+
+  /**
    * Returns the table definition for the routing fixtures.
    *
    * @return array
