diff --git a/core/core.services.yml b/core/core.services.yml
index 465d145..e53ab81 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -293,8 +293,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:
@@ -358,6 +363,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: ['@http_kernel', '@controller_resolver', '@string_translation', '@title_resolver']
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index 66a6215..61a84e9 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -57,7 +57,7 @@ public function execute(FlattenException $exception, Request $request) {
       return $this->$method($exception, $request);
     }
 
-    return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders());
+    return new Response('An error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders());
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php
new file mode 100644
index 0000000..19b5488
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/RouteMethodSubscriber.php
@@ -0,0 +1,48 @@
+<?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() {
+    $events[RoutingEvents::ALTER][] = 'onRouteBuilding';
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
index cad587b..8966ca8 100644
--- a/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
+++ b/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php
@@ -7,13 +7,14 @@
 
 namespace Drupal\Core\ParamConverter;
 
-use Symfony\Component\DependencyInjection\ContainerAware;
+use Drupal\Component\Utility\String;
 use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerAware;
 use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\Routing\RouteCollection;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Manages converter services for converting request parameters to full objects.
@@ -186,7 +187,10 @@ public function enhance(array $defaults, Request $request) {
       // converted in which case we throw a 404.
       $defaults[$name] = $this->getConverter($definition['converter'])->convert($defaults[$name], $definition, $name, $defaults, $request);
       if (!isset($defaults[$name])) {
-        throw new NotFoundHttpException();
+        throw new NotFoundHttpException(String::format('Item "@name" with ID @id not found', array(
+          '@name' => $name,
+          '@id' => $defaults['_raw_variables']->get($name),
+        )));
       }
     }
 
diff --git a/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
new file mode 100644
index 0000000..eee3056
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/AcceptHeaderMatcher.php
@@ -0,0 +1,88 @@
+<?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);
+
+    // Collect a list of routes that match the primary request content type.
+    $primary_matches = new RouteCollection();
+    // List of routes that match any of multiple specified content types in the
+    // request, which should get a lower priority.
+    $somehow_matches = new RouteCollection();
+
+    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.
+        $somehow_matches->add($name, $route);
+      }
+      elseif (in_array($primary_format, $supported_formats)) {
+        // Perfect match, which will get a higher priority.
+        $primary_matches->add($name, $route);
+      }
+      // 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)) {
+        $somehow_matches->add($name, $route);
+      }
+    }
+    // Append the generic routes to the end, which will give them a lower
+    // priority.
+    $primary_matches->addCollection($somehow_matches);
+
+    if (count($primary_matches)) {
+      return $primary_matches;
+    }
+
+    // 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..3b32bf9
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\Routing\ContentTypeHeaderMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Drupal\Core\ContentNegotiation;
+use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+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, so nothing
+    // to filter in this case.
+    if ($request->getMethod() == '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 400.
+    throw new BadRequestHttpException('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/hal/lib/Drupal/hal/HalSubscriber.php b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php
index 93a70bb..b770a74 100644
--- a/core/modules/hal/lib/Drupal/hal/HalSubscriber.php
+++ b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php
@@ -34,7 +34,8 @@ public function onKernelRequest(GetResponseEvent $event) {
    *   An array of event listener definitions.
    */
   static function getSubscribedEvents() {
-    $events[KernelEvents::REQUEST][] = array('onKernelRequest', 40);
+    // The format must be available before routing, so we make the priority big.
+    $events[KernelEvents::REQUEST][] = array('onKernelRequest', 4000);
     return $events;
   }
 
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 9bb2f99..88c67b6 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
@@ -74,6 +74,18 @@ public function getDerivativeDefinitions(array $base_plugin_definition) {
           'serialization_class' => $entity_info['class'],
           'label' => $entity_info['label'],
         );
+        // Use the entity links as REST URL patterns if available.
+        $this->derivatives[$entity_type]['links']['drupal:create'] = isset($entity_info['links']['drupal:create']) ? $entity_info['links']['drupal:create'] : "/entity/$entity_type";
+        // Replace the default cannonical link pattern with a version that
+        // directly uses the entity type, because we want only one parameter and
+        // automatic upcasting.
+        if ($entity_info['links']['canonical'] == '/entity/{entityType}/{id}') {
+          $this->derivatives[$entity_type]['links']['canonical'] = "/entity/$entity_type/" . '{' . $entity_type . '}';
+        }
+        else {
+          $this->derivatives[$entity_type]['links']['canonical'] = $entity_info['links']['canonical'];
+        }
+
         $this->derivatives[$entity_type] += $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..f0b78c0 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['links']['canonical']) ? $definition['links']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
+    $create_path = isset($definition['links']['drupal:create']) ? $definition['links']['drupal: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,14 @@ 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);
+          // Do not break here, fall through to PATCH additions which also apply
+          // to POST.
+
+        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 +119,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/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
index 2d2f353..d53687d 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
@@ -25,7 +25,11 @@
  *   id = "entity",
  *   label = @Translation("Entity"),
  *   serialization_class = "Drupal\Core\Entity\Entity",
- *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
+ *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative",
+ *   links = {
+ *     "canonical" = "/entity/{entity_type}/{entity}",
+ *     "drupal:create" = "/entity/{entity_type}"
+ *   }
  * )
  */
 class EntityResource extends ResourceBase {
@@ -33,36 +37,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.
    *
@@ -71,7 +68,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.'));
     }
@@ -114,8 +111,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.
    *
@@ -124,24 +121,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->entityType() != $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();
     }
@@ -176,33 +163,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->entityType(), '%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->entityType(), '%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 521d09e..5012be7 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 HAL.
     $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_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 2be8ff3..ae73e48 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php
@@ -48,7 +48,7 @@ public function testRead() {
     // 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->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.');
+    $this->assertText('An error occurred: No authentication credentials provided.');
 
     // Ensure that cURL settings/headers aren't carried over to next request.
     unset($this->curlHandle);
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index 10695c4..95b1dfd 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -50,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($this->entityBasePath($entity_type) . '/' . $entity->id(), 'DELETE');
       // Clear the static cache with entity_load(), otherwise we won't see the
       // update.
       $entity = entity_load($entity_type, $entity->id(), TRUE);
@@ -59,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($this->entityBasePath($entity_type) . '/9999', 'DELETE');
       $this->assertResponse(404);
-      $decoded = drupal_json_decode($response);
-      $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
+      $this->assertText('The requested page "/' . $this->entityBasePath($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($this->entityBasePath($entity_type) . '/' . $entity->id(), '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 76773df..65a7f1d 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/NodeTest.php
@@ -55,8 +55,15 @@ 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->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 +83,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 d6fc9f9..bfc2736 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -248,16 +248,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);
   }
 
   /**
@@ -295,4 +301,22 @@ protected function entityPermissions($entity_type, $operation) {
         }
     }
   }
+
+  /**
+   * Returns the base URI path of an entity type.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   *
+   * @return string
+   *   The URI path.
+   */
+  protected function entityBasePath($entity_type) {
+    switch ($entity_type) {
+      case 'entity_test':
+        return 'entity/entity_test';
+      case 'node':
+        return 'node';
+    }
+  }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index a22174e..61a0408 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -50,7 +50,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($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse('200', 'HTTP response code is correct.');
       $this->assertHeader('content-type', $this->defaultMimeType);
       $data = drupal_json_decode($response);
@@ -59,14 +59,22 @@ 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/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
-      $this->assertResponse(406);
+      $response = $this->httpRequest($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
+      if ($entity_type == 'entity_test') {
+        $this->assertResponse(406);
+        $this->assertEqual($response, 'An error occurred: No route found for the specified formats application/wrongformat.');
+      }
+      if ($entity_type == 'node') {
+        // Nodes are special because there are HTML routes without format
+        // restrictions, so we are hitting the standard node view route here.
+        $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($this->entityBasePath($entity_type) . '/9999', 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse(404);
-      $decoded = drupal_json_decode($response);
-      $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
+      $this->assertEqual($response, 'An error occurred: Item "' . $entity_type . '" with ID 9999 not found');
 
       // Make sure that field level access works and that the according field is
       // not available in the response. Only applies to entity_test.
@@ -74,7 +82,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($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
         $this->assertResponse(200);
         $this->assertHeader('content-type', $this->defaultMimeType);
         $data = drupal_json_decode($response);
@@ -83,7 +91,7 @@ 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($this->entityBasePath($entity_type) . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
       $this->assertResponse(403);
       $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
     }
@@ -113,7 +121,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('node/' . $entity->id(), 'GET', NULL, 'application/json');
     $this->assertResponse('200', 'HTTP response code is correct.');
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php
deleted file mode 100644
index 827c733..0000000
--- a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Drupal\system\Tests\Routing\MimeTypeMatcherTest.
- */
-
-namespace Drupal\system\Tests\Routing;
-
-use Drupal\Core\Routing\MimeTypeMatcher;
-use Drupal\simpletest\UnitTestBase;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
-
-/**
- * Basic tests for the MimeTypeMatcher class.
- */
-class MimeTypeMatcherTest extends UnitTestBase {
-
-  /**
-   * 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',
-    );
-  }
-
-  function __construct($test_id = NULL) {
-    parent::__construct($test_id);
-
-    $this->fixtures = new RoutingFixtures();
-  }
-
-  /**
-   * Confirms that the MimeType matcher matches properly.
-   */
-  public function testFilterRoutes() {
-
-    $matcher = new MimeTypeMatcher();
-    $collection = $this->fixtures->sampleRouteCollection();
-
-    // Tests basic JSON request.
-    $request = Request::create('path/two', 'GET');
-    $request->headers->set('Accept', 'application/json, text/xml;q=0.9');
-    $routes = $matcher->filter($collection, $request);
-    $this->assertEqual(count($routes), 4, 'The correct number of routes was found.');
-    $this->assertNotNull($routes->get('route_c'), 'The json route was found.');
-    $this->assertNull($routes->get('route_e'), 'The html route was not found.');
-
-    // Tests JSON request with alternative JSON MIME type Accept header.
-    $request = Request::create('path/two', 'GET');
-    $request->headers->set('Accept', 'application/x-json, text/xml;q=0.9');
-    $routes = $matcher->filter($collection, $request);
-    $this->assertEqual(count($routes), 4, 'The correct number of routes was found.');
-    $this->assertNotNull($routes->get('route_c'), 'The json route was found.');
-    $this->assertNull($routes->get('route_e'), 'The html route was not found.');
-
-    // Tests basic HTML request.
-    $request = Request::create('path/two', 'GET');
-    $request->headers->set('Accept', 'text/html, text/xml;q=0.9');
-    $routes = $matcher->filter($collection, $request);
-    $this->assertEqual(count($routes), 4, 'The correct number of routes was found.');
-    $this->assertNull($routes->get('route_c'), 'The json route was not found.');
-    $this->assertNotNull($routes->get('route_e'), 'The html route was found.');
-  }
-
-  /**
-   * Confirms that the MimeTypeMatcher matcher throws an exception for no-route.
-   */
-  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');
-
-    try {
-      $request = Request::create('path/two', 'GET');
-      $request->headers->set('Accept', 'application/json, text/xml;q=0.9');
-      $routes = $matcher->filter($routes, $request);
-      $this->fail(t('No exception was thrown.'));
-    }
-    catch (NotAcceptableHttpException $e) {
-      $this->pass('The correct exception was thrown.');
-    }
-  }
-
-}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php
index 06f0953..353a987 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php
@@ -106,6 +106,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
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..2fb63cb
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Routing/AcceptHeaderMatcherTest.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest.
+ */
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Routing\AcceptHeaderMatcher;
+use Drupal\system\Tests\Routing\RoutingFixtures;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Basic tests for the AcceptHeaderMatcher class.
+ */
+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;
+
+  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() {
+    parent::setUp();
+
+    $this->fixtures = new RoutingFixtures();
+    $this->matcher = new AcceptHeaderMatcher(new ContentNegotiation());
+  }
+
+  /**
+   * Check that JSON routes get filtered and prioritized correctly.
+   */
+  public function testJsonFilterRoutes() {
+    $collection = $this->fixtures->sampleRouteCollection();
+
+    // Tests basic JSON request.
+    $request = Request::create('path/two', 'GET');
+    $request->headers->set('Accept', 'application/json, text/xml;q=0.9');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 4, 'The correct number of routes was found.');
+    $this->assertNotNull($routes->get('route_c'), 'The json route was found.');
+    $this->assertNull($routes->get('route_e'), 'The html route was not found.');
+    foreach ($routes as $name => $route) {
+      $this->assertEquals($name, 'route_c', 'The json route is the first one in the collection.');
+      break;
+    }
+  }
+
+  /**
+   * Tests a JSON request with alternative JSON MIME type Accept header.
+   */
+  public function testAlternativeJson() {
+    $collection = $this->fixtures->sampleRouteCollection();
+
+    $request = Request::create('path/two', 'GET');
+    $request->headers->set('Accept', 'application/x-json, text/xml;q=0.9');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 4, 'The correct number of routes was found.');
+    $this->assertNotNull($routes->get('route_c'), 'The json route was found.');
+    $this->assertNull($routes->get('route_e'), 'The html route was not found.');
+    foreach ($routes as $name => $route) {
+      $this->assertEquals($name, 'route_c', 'The json route is the first one in the collection.');
+      break;
+    }
+  }
+
+  /**
+   * Tests a standard HTML request.
+   */
+  public function teststandardHtml() {
+    $collection = $this->fixtures->sampleRouteCollection();
+
+    $request = Request::create('path/two', 'GET');
+    $request->headers->set('Accept', 'text/html, text/xml;q=0.9');
+    $routes = $this->matcher->filter($collection, $request);
+    $this->assertEquals(count($routes), 4, 'The correct number of routes was found.');
+    $this->assertNull($routes->get('route_c'), 'The json route was not found.');
+    $this->assertNotNull($routes->get('route_e'), 'The html route was found.');
+    foreach ($routes as $name => $route) {
+      $this->assertEquals($name, 'route_e', 'The html route is the first one in the collection.');
+      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..2b8990f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Routing\ContentTypeHeaderMatcherTest.
+ */
+
+namespace Drupal\Tests\Core\Routing;
+
+use Drupal\Core\Routing\ContentTypeHeaderMatcher;
+use Drupal\system\Tests\Routing\RoutingFixtures;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Basic tests for the ContentTypeHeaderMatcher class.
+ */
+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;
+
+  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',
+    );
+  }
+
+  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\BadRequestHttpException
+   * @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.');
+  }
+
+}
