diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
index 2a72267..cdaf107 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -26,7 +26,8 @@ class AjaxResponse extends JsonResponse {
    * Add an AJAX command to the response.
    *
    * @param object $command
-   *   An AJAX command object implementing CommandInterface.
+   *   An AJAX command object implementing CommandInterface or a rendered
+   *   command (an array).
    * @param boolean $prepend
    *   A boolean which determines whether the new command should be executed
    *   before previously added commands. Defaults to FALSE.
@@ -35,11 +36,14 @@ class AjaxResponse extends JsonResponse {
    *   The current AjaxResponse.
    */
   public function addCommand($command, $prepend = FALSE) {
+    // @todo Remove support for already rendered commands once all core ajax
+    //   callbacks are are converted.
+    $rendered_command = is_array($command) ? $command : $command->render();
     if ($prepend) {
-      array_unshift($this->commands, $command->render());
+      array_unshift($this->commands, $rendered_command);
     }
     else {
-      $this->commands[] = $command->render();
+      $this->commands[] = $rendered_command;
     }
 
     return $this;
diff --git a/core/lib/Drupal/Core/AjaxController.php b/core/lib/Drupal/Core/AjaxController.php
index 31259fb..c423489 100644
--- a/core/lib/Drupal/Core/AjaxController.php
+++ b/core/lib/Drupal/Core/AjaxController.php
@@ -12,8 +12,6 @@
 use Drupal\Core\Ajax\PrependCommand;
 use Symfony\Component\DependencyInjection\ContainerAware;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Default controller for ajax requests.
@@ -56,7 +54,18 @@ public function content(Request $request, $_content) {
     if ($response->isOk()) {
       // If there is already an AjaxResponse, then return it without
       // manipulation.
-      if (!($response instanceof AjaxResponse)) {
+      if ($response instanceof AjaxResponse) {
+        return $response;
+      }
+      elseif (is_array($response)) {
+        // Add all commands to the ajax response.
+        $ajax_response = new AjaxResponse();
+        foreach ($response['#commands'] as $command) {
+          $ajax_response->addCommand($command);
+        }
+        $response = $ajax_response;
+      }
+      else {
         // Pull the content out of the response.
         $content = $response->getContent();
         // A page callback could return a render array or a string.
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index ee63bfa..5d3839c 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -230,15 +230,10 @@ public function build(ContainerBuilder $container) {
     $container->register('mime_type_matcher', 'Drupal\Core\Routing\MimeTypeMatcher')
       ->addTag('route_filter');
 
-    $container->register('paramconverter_manager', 'Drupal\Core\ParamConverter\ParamConverterManager')
-      ->addTag('route_enhancer');
     $container->register('paramconverter.entity', 'Drupal\Core\ParamConverter\EntityConverter')
       ->addArgument(new Reference('plugin.manager.entity'))
       ->addTag('paramconverter');
 
-    $container->register('router_processor_subscriber', 'Drupal\Core\EventSubscriber\RouteProcessorSubscriber')
-      ->addArgument(new Reference('content_negotiation'))
-      ->addTag('event_subscriber');
     $container->register('router_listener', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
       ->addArgument(new Reference('router'))
       ->addTag('event_subscriber');
@@ -322,7 +317,6 @@ public function build(ContainerBuilder $container) {
     $container->addCompilerPass(new RegisterAccessChecksPass());
     // Add a compiler pass for upcasting of entity route parameters.
     $container->addCompilerPass(new RegisterParamConvertersPass());
-    $container->addCompilerPass(new RegisterRouteEnhancersPass());
     // Add a compiler pass for registering services needing destruction.
     $container->addCompilerPass(new RegisterServicesForDestructionPass());
   }
@@ -380,6 +374,23 @@ protected function registerRouting(ContainerBuilder $container) {
       ->addMethodCall('setContext', array(new Reference('router.request_context')))
       ->addMethodCall('add', array(new Reference('router.dynamic')))
       ->addMethodCall('add', array(new Reference('legacy_router')));
+
+    // Add a route enhancer to upcast parameters to objects if possible.
+    $container->register('paramconverter_manager', 'Drupal\Core\ParamConverter\ParamConverterManager')
+      ->addTag('route_enhancer', array('priority' => 50));
+
+    // Add core route enhancers to dynamically derive the _controller
+    $container->register('route_enhancer.ajax', 'Drupal\Core\Routing\Enhancer\AjaxEnhancer')
+      ->addArgument(new Reference('content_negotiation'))
+      ->addTag('route_enhancer', array('priority' => 20));
+    $container->register('route_enhancer.form', 'Drupal\Core\Routing\Enhancer\FormEnhancer')
+      ->addArgument(new Reference('content_negotiation'))
+      ->addTag('route_enhancer', array('priority' => 10));
+    $container->register('route_enhancer.page', 'Drupal\Core\Routing\Enhancer\PageEnhancer')
+      ->addArgument(new Reference('content_negotiation'))
+      ->addTag('route_enhancer', array('priority' => 0));
+
+    $container->addCompilerPass(new RegisterRouteEnhancersPass());
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php
deleted file mode 100644
index 7b1e34a..0000000
--- a/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\Core\EventSubscriber\RouteProcessorSubscriber.
- */
-
-namespace Drupal\Core\EventSubscriber;
-
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
-use Symfony\Component\HttpKernel\Log\LoggerInterface;
-use Symfony\Component\HttpKernel\KernelEvents;
-use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
-use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Drupal\Core\ContentNegotiation;
-
-/**
- * Listener to process request controller information.
- */
-class RouteProcessorSubscriber implements EventSubscriberInterface {
-
-  protected $negotiation;
-
-  public function __construct(ContentNegotiation $negotiation) {
-    $this->negotiation = $negotiation;
-  }
-
-  /**
-   * Sets a default controller for a route if one was not specified.
-   *
-   * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
-   *   Event that is created to create a response for a request.
-   */
-  public function onRequestSetController(GetResponseEvent $event) {
-    $request = $event->getRequest();
-
-    // @todo This should all get converted into one or more RouteEnhancers.
-    if (!$request->attributes->has('_content') && $this->negotiation->getContentType($request) == 'drupal_ajax') {
-      $request->attributes->set('_content', $request->attributes->get('_controller'));
-      $request->attributes->set('_controller', '\Drupal\Core\AjaxController::content');
-    }
-    elseif (!$request->attributes->has('_controller') && $this->negotiation->getContentType($request) === 'html') {
-      if ($request->attributes->has('_form')) {
-        $request->attributes->set('_controller', '\Drupal\Core\HtmlFormController::content');
-      }
-      elseif ($request->attributes->has('_content')) {
-        $request->attributes->set('_controller', '\Drupal\Core\HtmlPageController::content');
-      }
-      else {
-        throw new \InvalidArgumentException('No valid subcontroller key was found');
-      }
-    }
-  }
-
-  /**
-   * Registers the methods in this class that should be listeners.
-   *
-   * @return array
-   *   An array of event listener definitions.
-   */
-  static function getSubscribedEvents() {
-    // The RouterListener has priority 32, and we need to run after that.
-    $events[KernelEvents::REQUEST][] = array('onRequestSetController', 30);
-
-    return $events;
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxController.php b/core/lib/Drupal/Core/Form/FormAjaxController.php
new file mode 100644
index 0000000..c2c23fd
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxController.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxController.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+
+/**
+ * Handles Ajax requests for the #ajax Form API property.
+ *
+ * @todo How to handle 'ajax_base_page_theme' theme callback.
+ */
+class FormAjaxController {
+
+  /**
+   * This rebuilds the form from cache and invokes the defined #ajax['callback']
+   * to return an Ajax command structure for JavaScript. In case no 'callback' has
+   * been defined, nothing will happen.
+   *
+   * The Form API #ajax property can be set both for buttons and other input
+   * elements.
+   *
+   * This function is also the canonical example of how to implement
+   * #ajax['path']. If processing is required that cannot be accomplished with
+   * a callback, re-implement this function and set #ajax['path'] to the
+   * enhanced function.
+   */
+  public function content() {
+    list($form, $form_state) = ajax_get_form();
+    drupal_process_form($form['#form_id'], $form, $form_state);
+
+    // We need to return the part of the form (or some other content) that needs
+    // to be re-rendered so the browser can update the page with changed content.
+    // Since this is the generic menu callback used by many Ajax elements, it is
+    // up to the #ajax['callback'] function of the element (may or may not be a
+    // button) that triggered the Ajax request to determine what needs to be
+    // rendered.
+    if (!empty($form_state['triggering_element'])) {
+      $callback = $form_state['triggering_element']['#ajax']['callback'];
+    }
+    if (!empty($callback) && is_callable($callback)) {
+      return call_user_func_array($callback, array(&$form, &$form_state));
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php
new file mode 100644
index 0000000..30cc449
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Routing\Enhancer\AjaxEnhancer.
+ */
+
+namespace Drupal\Core\Routing\Enhancer;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
+use Drupal\Core\ContentNegotiation;
+
+/**
+ * Enhances an ajax route with the appropriate controller.
+ */
+class AjaxEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * Content negotiation library.
+   *
+   * @var \Drupal\CoreContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Constructs a new \Drupal\Core\Routing\Enhancer\AjaxEnhancer object.
+   */
+  public function __construct(ContentNegotiation $negotiation) {
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance()
+   */
+  public function enhance(array $defaults, Request $request) {
+    // Old-style routes work differently, since they are their own controller.
+    if ($request->attributes->get('_legacy') == TRUE) {
+      if (empty($defaults['_content']) && $this->negotiation->getContentType($request) == 'drupal_ajax') {
+        $defaults['_content'] = $defaults['_controller'];
+        $defaults['_controller'] = '\Drupal\Core\AjaxController::content';
+      }
+    }
+    elseif (!empty($defaults['_controller']) && empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'drupal_ajax') {
+      $defaults['_content'] = $defaults['_controller'];
+      $defaults['_controller'] = '\Drupal\Core\AjaxController::content';
+    }
+    else {
+      if (empty($defaults['_controller']) && !empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'drupal_ajax') {
+        $defaults['_controller'] = '\Drupal\Core\AjaxController::content';
+      }
+    }
+    return $defaults;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php
new file mode 100644
index 0000000..2210d03
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/Enhancer/FormEnhancer.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Routing\Enhancer\FormEnhancer.
+ */
+
+namespace Drupal\Core\Routing\Enhancer;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
+use Drupal\Core\ContentNegotiation;
+
+/**
+ * Enhances a form route with the appropriate controller.
+ */
+class FormEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * Content negotiation library.
+   *
+   * @var \Drupal\CoreContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Constructs a new \Drupal\Core\Routing\Enhancer\FormEnhancer object.
+   */
+  public function __construct(ContentNegotiation $negotiation) {
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance()
+   */
+  public function enhance(array $defaults, Request $request) {
+    if (empty($defaults['_controller']) && !empty($defaults['_form']) && $this->negotiation->getContentType($request) === 'html') {
+      $defaults['_controller'] = '\Drupal\Core\HtmlFormController::content';
+    }
+    return $defaults;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php
new file mode 100644
index 0000000..3d73e9e
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/Enhancer/PageEnhancer.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Routing\Enhancer\PageEnhancer.
+ */
+
+namespace Drupal\Core\Routing\Enhancer;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface;
+use Drupal\Core\ContentNegotiation;
+
+/**
+ * Enhances a page route with the appropriate controller.
+ */
+class PageEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * Content negotiation library.
+   *
+   * @var \Drupal\CoreContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Constructs a new \Drupal\Core\Routing\Enhancer\PageEnhancer object.
+   */
+  public function __construct(ContentNegotiation $negotiation) {
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * Implements \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface::enhance()
+   */
+  public function enhance(array $defaults, Request $request) {
+    if (empty($defaults['_controller']) && !empty($defaults['_content']) && $this->negotiation->getContentType($request) === 'html') {
+      $defaults['_controller'] = '\Drupal\Core\HtmlPageController::content';
+    }
+    return $defaults;
+  }
+
+}
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 5cca38a..e3d2274 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1360,7 +1360,8 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
    *   (optional) Options to be forwarded to url().
    * @param $headers
    *   (optional) An array containing additional HTTP request headers, each
-   *   formatted as "name: value". Forwarded to drupalPost().
+   *   formatted as "name: value". Forwarded to drupalPost(). Defaults to
+   *   array('Accept: application/vnd.drupal-ajax').
    * @param $form_html_id
    *   (optional) HTML ID of the form to be submitted, use when there is more
    *   than one identical form on the same page and the value of the triggering
@@ -1377,6 +1378,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
    * @see ajax.js
    */
   protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
+    $headers += array('Accept: application/vnd.drupal-ajax');
     // Get the content of the initial page prior to calling drupalPost(), since
     // drupalPost() replaces $this->content.
     if (isset($path)) {
@@ -1411,7 +1413,7 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
     }
     $ajax_html_ids = array();
     foreach ($this->xpath('//*[@id]') as $element) {
-      $ajax_html_ids[] = (string) $element['id'];
+       $ajax_html_ids[] = (string) $element['id'];
     }
     if (!empty($ajax_html_ids)) {
       $extra_post .= '&' . urlencode('ajax_html_ids') . '=' . urlencode(implode(' ', $ajax_html_ids));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
index 8a3f45f..352e89f 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
@@ -106,4 +106,35 @@ public function testDynamicRoutes() {
     $this->assertResponse(200);
     $this->assertRaw('test5', 'The correct string was returned because the route was successful.');
   }
+
+  /**
+   * Checks that a request with text/html response gets rendered as a page.
+   */
+  public function testControllerResolutionPage() {
+    $this->drupalGet('/router_test/test10');
+
+    $this->assertRaw('abcde', 'Correct body was found.');
+
+    // Confirm that the page wrapping is being added, so we're not getting a
+    // raw body returned.
+    $this->assertRaw('</html>', 'Page markup was found.');
+
+    // In some instances, the subrequest handling may get confused and render
+    // a page inception style.  This test verifies that is not happening.
+    $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+  }
+
+  /**
+   * Checks that an ajax request gets rendered as an Ajax response, by mime.
+   */
+  public function testControllerResolutionAjax() {
+    // This will fail with a JSON parse error if the request is not routed to
+    // The correct controller.
+    $this->drupalGetAJAX('/router_test/test10', array(), array('Accept: application/vnd.drupal-ajax'));
+
+    $this->assertEqual($this->drupalGetHeader('Content-Type'), 'application/json', 'Correct mime content type was returned');
+
+    $this->assertRaw('abcde', 'Correct body was found.');
+  }
+
 }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index c4304f4..44ab0a8 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -627,15 +627,6 @@ function system_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
-  $items['system/ajax'] = array(
-    'title' => 'AHAH callback',
-    'page callback' => 'ajax_form_callback',
-    'access callback' => TRUE,
-    'theme callback' => 'ajax_base_page_theme',
-    'type' => MENU_CALLBACK,
-    'file path' => 'core/includes',
-    'file' => 'form.inc',
-  );
   $items['system/timezone'] = array(
     'title' => 'Time zone',
     'page callback' => 'system_timezone',
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 085895e..a2a641e 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -4,3 +4,10 @@ system.cron:
     _controller: '\Drupal\system\CronController::run'
   requirements:
     _access_system_cron: 'TRUE'
+
+system.form_ajax:
+  pattern: '/system/ajax'
+  defaults:
+    _controller: '\Drupal\Core\Form\FormAjaxController::content'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php
new file mode 100644
index 0000000..11fc1b0
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\router_test\TestContent.
+ */
+
+namespace Drupal\router_test;
+
+/**
+ * Test controllers that are intended to be wrapped in a main controller.
+ */
+class TestContent {
+
+  public function test1() {
+    return 'abcde';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml
index 95b0cec..2a989df 100644
--- a/core/modules/system/tests/modules/router_test/router_test.routing.yml
+++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml
@@ -53,3 +53,10 @@ router_test_9:
   requirements:
     _permission: 'access test7'
     _access_router_test: 'TRUE'
+
+router_test_10:
+  pattern: '/router_test/test10'
+  defaults:
+    _content: '\Drupal\router_test\TestContent::test1'
+  requirements:
+    _access: 'TRUE'
