diff --git a/core/core.services.yml b/core/core.services.yml
index 589c5f6..6948cd6 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -815,6 +815,11 @@ services:
     class: Drupal\Core\EventSubscriber\AjaxSubscriber
     tags:
       - { name: event_subscriber }
+  form_ajax_subscriber:
+    class: Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
+    arguments: ['@form_ajax_response_builder']
+    tags:
+      - { name: event_subscriber }
   route_enhancer.lazy_collector:
     class: Drupal\Core\Routing\LazyRouteEnhancer
     tags:
@@ -885,6 +890,9 @@ services:
   controller.entity_form:
     class: Drupal\Core\Entity\HtmlEntityFormController
     arguments: ['@controller_resolver', '@form_builder', '@entity.manager']
+  form_ajax_response_builder:
+    class: Drupal\Core\Form\FormAjaxResponseBuilder
+    arguments: ['@main_content_renderer.ajax', '@current_route_match']
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
diff --git a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php
new file mode 100644
index 0000000..04ec605
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber.
+ */
+
+namespace Drupal\Core\Form\EventSubscriber;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Form\FormAjaxException;
+use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Wraps AJAX form submissions that are triggered via an exception.
+ */
+class FormAjaxSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The form AJAX response builder.
+   *
+   * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
+   */
+  protected $formAjaxResponseBuilder;
+
+  /**
+   * Constructs a new FormAjaxSubscriber.
+   *
+   * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
+   *   The form AJAX response builder.
+   */
+  public function __construct(FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
+    $this->formAjaxResponseBuilder = $form_ajax_response_builder;
+  }
+
+  /**
+   * Alters the wrapper format if this is an AJAX form request.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
+   *   The event to process.
+   */
+  public function onView(GetResponseForControllerResultEvent $event) {
+    // To support an AJAX form submission of a form within a block, make the
+    // later VIEW subscribers process the controller result as though for
+    // HTML display (i.e., add blocks). During that block building, when the
+    // submitted form gets processed, an exception gets thrown by
+    // \Drupal\Core\Form\FormBuilderInterface::buildForm(), allowing
+    // self::onException() to return an AJAX response instead of an HTML one.
+    $request = $event->getRequest();
+    if ($request->query->has(FormBuilderInterface::AJAX_FORM_REQUEST)) {
+      $request->query->set(MainContentViewSubscriber::WRAPPER_FORMAT, 'html');
+    }
+  }
+
+  /**
+   * Catches a form AJAX exception and build a response from it.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function onException(GetResponseForExceptionEvent $event) {
+    // Extract the form AJAX exception (it may have been passed to another
+    // exception before reaching here).
+    if ($exception = $this->getFormAjaxException($event->getException())) {
+      $request = $event->getRequest();
+      $form = $exception->getForm();
+      $form_state = $exception->getFormState();
+
+      // Set the build ID from the request as the old build ID on the form.
+      $form['#build_id_old'] = $request->get('form_build_id');
+
+      try {
+        $response = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, []);
+
+        // Since this response is being set in place of an exception, explicitly
+        // mark this as a 200 status.
+        $response->headers->set('X-Status-Code', 200);
+        $event->setResponse($response);
+      }
+      catch (\Exception $e) {
+        // Otherwise, replace the existing exception with the new one.
+        $event->setException($e);
+      }
+    }
+  }
+
+  /**
+   * Extracts a form AJAX exception.
+   *
+   * @param \Exception $e
+   *  A generic exception that might contain a form AJAX exception.
+   *
+   * @return \Drupal\Core\Form\FormAjaxException|null
+   *   Either the form AJAX exception, or NULL if none could be found.
+   */
+  protected function getFormAjaxException(\Exception $e) {
+    $exception = NULL;
+    while ($e) {
+      if ($e instanceof FormAjaxException) {
+        $exception = $e;
+        break;
+      }
+
+      $e = $e->getPrevious();
+    }
+    return $exception;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run before exception.logger.
+    $events[KernelEvents::EXCEPTION] = ['onException', 51];
+    // Run before main_content_view_subscriber.
+    $events[KernelEvents::VIEW][] = ['onView', 1];
+
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxException.php b/core/lib/Drupal/Core/Form/FormAjaxException.php
new file mode 100644
index 0000000..e9062bb
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxException.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxException.
+ */
+
+namespace Drupal\Core\Form;
+
+/**
+ * Custom exception to break out of AJAX form processing.
+ */
+class FormAjaxException extends \Exception {
+
+  /**
+   * The form definition.
+   *
+   * @var array
+   */
+  protected $form;
+
+  /**
+   * The form state.
+   *
+   * @var \Drupal\Core\Form\FormStateInterface
+   */
+  protected $formState;
+
+  /**
+   * Constructs a FormAjaxException object.
+   *
+   * @param array $form
+   *   The form definition.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param string $message
+   *   (optional) The exception message.
+   * @param int $code
+   *   (optional) A user defined exception code.
+   * @param \Exception $previous
+   *   (optional) The previous exception for nested exceptions.
+   */
+  public function __construct(array $form, FormStateInterface $form_state, $message = "", $code = 0, \Exception $previous = NULL) {
+    parent::__construct($message, $code, $previous);
+    $this->form = $form;
+    $this->formState = $form_state;
+  }
+
+  /**
+   * Gets the form definition.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function getForm() {
+    return $this->form;
+  }
+
+  /**
+   * Gets the form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   *   The current state of the form.
+   */
+  public function getFormState() {
+    return $this->formState;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php
new file mode 100644
index 0000000..7d1c4b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseBuilder.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\UpdateBuildIdCommand;
+use Drupal\Core\Render\MainContent\MainContentRendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Builds an AJAX form response.
+ *
+ * Given the current request, a form render array, its form state, and any AJAX
+ * commands to apply to the form, build a response object.
+ */
+class FormAjaxResponseBuilder implements FormAjaxResponseBuilderInterface {
+
+  /**
+   * The main content to AJAX Response renderer.
+   *
+   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
+   */
+  protected $ajaxRenderer;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a new FormAjaxResponseBuilder.
+   *
+   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
+   *   The ajax renderer.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
+    $this->ajaxRenderer = $ajax_renderer;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands) {
+    // If the form build ID has changed, issue an Ajax command to update it.
+    if (isset($form['#build_id_old']) && $form['#build_id_old'] !== $form['#build_id']) {
+      $commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']);
+    }
+
+    // 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. 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.
+    $callback = NULL;
+    if (($triggering_element = $form_state->getTriggeringElement()) && isset($triggering_element['#ajax']['callback'])) {
+      $callback = $triggering_element['#ajax']['callback'];
+    }
+    $callback = $form_state->prepareCallback($callback);
+    if (empty($callback) || !is_callable($callback)) {
+      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
+    }
+    $result = call_user_func_array($callback, [&$form, &$form_state, $request]);
+
+    // If the callback is an #ajax callback, the result is a render array, and
+    // we need to turn it into an AJAX response, so that we can add any commands
+    // we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
+    // submit from a cached page.
+    if ($result instanceof AjaxResponse) {
+      $response = $result;
+    }
+    else {
+      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
+      $response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
+    }
+
+    foreach ($commands as $command) {
+      $response->addCommand($command, TRUE);
+    }
+    return $response;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php
new file mode 100644
index 0000000..720ecc8
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseBuilderInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseBuilderInterface.
+ */
+
+namespace Drupal\Core\Form;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an interface for building AJAX form responses.
+ */
+interface FormAjaxResponseBuilderInterface {
+
+  /**
+   * Builds a response for an AJAX form.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $commands
+   *   An array of AJAX commands to apply to the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response representing the form and its AJAX commands.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   *   Thrown if the AJAX callback is not a callable.
+   */
+  public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands);
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 4b995c1..1245c6f 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -244,6 +244,12 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
       }
     }
 
+    // If this form is an AJAX request, disable all form redirects.
+    $request = $this->requestStack->getCurrentRequest();
+    if ($ajax_form_request = $request->query->has(static::AJAX_FORM_REQUEST)) {
+      $form_state->disableRedirect();
+    }
+
     // Now that we have a constructed form, process it. This is where:
     // - Element #process functions get called to further refine $form.
     // - User input, if any, gets incorporated in the #value property of the
@@ -257,6 +263,17 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
     // can use it to know or update information about the state of the form.
     $response = $this->processForm($form_id, $form, $form_state);
 
+    // After processing the form, if this is an AJAX form request, interrupt
+    // form rendering and return by throwing an exception that contains the
+    // processed form and form state. This exception will be caught by
+    // \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException() and
+    // then passed through
+    // \Drupal\Core\Form\FormAjaxResponseBuilderInterface::buildResponse() to
+    // build a proper AJAX response.
+    if ($ajax_form_request && $form_state->isProcessingInput()) {
+      throw new FormAjaxException($form, $form_state);
+    }
+
     // If the form returns a response, skip subsequent page construction by
     // throwing an exception.
     // @see Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 5f8b452..60ad704 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -13,6 +13,21 @@
 interface FormBuilderInterface {
 
   /**
+   * Request key for AJAX forms that submit to the form's original route.
+   *
+   * This constant is distinct from a "drupal_ajax" value for
+   * \Drupal\Core\EventSubscriber\MainContentViewSubscriber::WRAPPER_FORMAT,
+   * because that one is set for all AJAX submissions, including ones with
+   * dedicated routes for which self::buildForm() should not exit early via a
+   * \Drupal\Core\Form\FormAjaxException.
+   *
+   * @todo Re-evaluate the need for this constant after
+   *   https://www.drupal.org/node/2502785 and
+   *   https://www.drupal.org/node/2503429.
+   */
+  const AJAX_FORM_REQUEST = 'ajax_form';
+
+  /**
    * Determines the ID of a form.
    *
    * @param \Drupal\Core\Form\FormInterface|string $form_arg
@@ -69,6 +84,14 @@ public function getForm($form_arg);
    *   The rendered form. This function may also perform a redirect and hence
    *   may not return at all depending upon the $form_state flags that were set.
    *
+   * @throws \Drupal\Core\Form\FormAjaxException
+   *   Thrown when a form is triggered via an AJAX submission. It will be
+   *   handled by \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber.
+   * @throws \Drupal\Core\Form\EnforcedResponseException
+   *   Thrown when a form builder returns a response directly, usually a
+   *   \Symfony\Component\HttpFoundation\RedirectResponse. It will be handled by
+   *   \Drupal\Core\EventSubscriber\EnforcedFormResponseSubscriber.
+   *
    * @see self::redirectForm()
    */
   public function buildForm($form_id, FormStateInterface &$form_state);
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index c886fec..47c35d0 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Render\Element;
@@ -128,7 +129,10 @@ public static function preRenderGroup($element) {
    */
   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
     $element = static::preRenderAjaxForm($element);
-    if (!empty($element['#ajax_processed'])) {
+
+    // If the element was processed as an #ajax element, and a custom URL was
+    // provided, set the form to be cached.
+    if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) {
       $form_state->setCached();
     }
     return $element;
@@ -236,12 +240,22 @@ public static function preRenderAjaxForm($element) {
       // to be substantially different for a JavaScript triggered submission.
       // One such substantial difference is form elements that use
       // #ajax['callback'] for determining which part of the form needs
-      // re-rendering. For that, we have a special 'system/ajax' route.
-      $settings += array(
-        'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
-        'options' => array(),
+      // re-rendering. For that, we have a special 'system.ajax' route which
+      // must be manually set.
+      $settings += [
+        'url' => NULL,
+        'options' => ['query' => []],
         'dialogType' => 'ajax',
-      );
+      ];
+      if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
+        $settings['url'] = Url::fromRoute('<current>');
+        // Add all the current query parameters in order to ensure that we build
+        // the same form on the Ajax POST requests. For example the AccountForm
+        // takes query parameters into account in order to hide the password
+        // field dynamically.
+        $settings['options']['query'] += \Drupal::request()->query->all();
+        $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+      }
 
       // @todo Legacy support. Remove in Drupal 8.
       if (isset($settings['method']) && $settings['method'] == 'replace') {
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 3cb1c8d..0309dc2 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -328,20 +328,6 @@
       }
       else if (this.element && element.form) {
         this.url = this.$form.attr('action');
-
-        // @todo If there's a file input on this form, then jQuery will submit
-        //   the AJAX response with a hidden Iframe rather than the XHR object.
-        //   If the response to the submission is an HTTP redirect, then the
-        //   Iframe will follow it, but the server won't content negotiate it
-        //   correctly, because there won't be an ajax_iframe_upload POST
-        //   variable. Until we figure out a work around to this problem, we
-        //   prevent AJAX-enabling elements that submit to the same URL as the
-        //   form when there's a file input. For example, this means the Delete
-        //   button on the edit form of an Article node doesn't open its
-        //   confirmation form in a dialog.
-        if (this.$form.find(':file').length) {
-          return;
-        }
       }
     }
 
@@ -421,7 +407,8 @@
     else {
       ajax.options.url += '&';
     }
-    ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=drupal_' + (element_settings.dialogType || 'ajax');
+    // @todo Passing both always is kinda redicilous!
+    ajax.options.url += Drupal.ajax.AJAX_FORM_REQUEST + '=1&' + Drupal.ajax.WRAPPER_FORMAT + '=drupal_' + (element_settings.dialogType || 'ajax');
 
     // Bind the ajaxSubmit function to the element event.
     $(ajax.element).on(element_settings.event, function (event) {
@@ -446,6 +433,11 @@
   };
 
   /**
+   * Request key for a form that was triggered via AJAX.
+   */
+   Drupal.ajax.AJAX_FORM_REQUEST = 'ajax_form';
+
+  /**
    * URL query attribute to indicate the wrapper used to render a request.
    *
    * The wrapper format determines how the HTML is wrapped, for example in a
diff --git a/core/modules/field/src/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php
index 564e300..afa2987 100644
--- a/core/modules/field/src/Tests/FormTest.php
+++ b/core/modules/field/src/Tests/FormTest.php
@@ -435,7 +435,7 @@ function testFieldFormJSAddMore() {
     // Press 'add more' button through Ajax, and place the expected HTML result
     // as the tested content.
     $commands = $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_add_more');
-    $this->setRawContent($commands[1]['data']);
+    $this->setRawContent($commands[2]['data']);
 
     for ($delta = 0; $delta <= $delta_range; $delta++) {
       $this->assertFieldByName("{$field_name}[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml
index d9d4efa..b7d2e6a 100644
--- a/core/modules/file/file.routing.yml
+++ b/core/modules/file/file.routing.yml
@@ -1,12 +1,3 @@
-file.ajax_upload:
-  path: '/file/ajax'
-  defaults:
-    _controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
-  options:
-    _theme: ajax_base_page
-  requirements:
-    _permission: 'access content'
-
 file.ajax_progress:
   path: '/file/progress'
   defaults:
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index 90176f8..ff0c537 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -7,13 +7,8 @@
 
 namespace Drupal\file\Controller;
 
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\system\Controller\FormAjaxController;
 use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 
 /**
  * Defines a controller to respond to file widget AJAX requests.
@@ -21,74 +16,6 @@
 class FileWidgetAjaxController extends FormAjaxController {
 
   /**
-   * Processes AJAX file uploads and deletions.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request object.
-   *
-   * @return \Drupal\Core\Ajax\AjaxResponse
-   *   An AjaxResponse object.
-   */
-  public function upload(Request $request) {
-    $form_parents = explode('/', $request->query->get('element_parents'));
-    $form_build_id = $request->query->get('form_build_id');
-    $request_form_build_id = $request->request->get('form_build_id');
-
-    if (empty($request_form_build_id) || $form_build_id !== $request_form_build_id) {
-      // Invalid request.
-      drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
-      $response = new AjaxResponse();
-      $status_messages = array('#type' => 'status_messages');
-      return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
-    }
-
-    try {
-      /** @var $ajaxForm \Drupal\system\FileAjaxForm */
-      $ajaxForm = $this->getForm($request);
-      $form = $ajaxForm->getForm();
-      $form_state = $ajaxForm->getFormState();
-      $commands = $ajaxForm->getCommands();
-    }
-    catch (HttpExceptionInterface $e) {
-      // Invalid form_build_id.
-      drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
-      $response = new AjaxResponse();
-      $status_messages = array('#type' => 'status_messages');
-      return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
-    }
-
-    // Get the current element and count the number of files.
-    $current_element = NestedArray::getValue($form, $form_parents);
-    $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
-
-    // Process user input. $form and $form_state are modified in the process.
-    $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
-
-    // Retrieve the element to be rendered.
-    $form = NestedArray::getValue($form, $form_parents);
-
-    // Add the special Ajax class if a new file was added.
-    if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
-      $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
-    }
-    // Otherwise just add the new content class on a placeholder.
-    else {
-      $form['#suffix'] .= '<span class="ajax-new-content"></span>';
-    }
-
-    $status_messages = array('#type' => 'status_messages');
-    $form['#prefix'] .= $this->renderer->renderRoot($status_messages);
-    $output = $this->renderer->renderRoot($form);
-
-    $response = new AjaxResponse();
-    $response->setAttachments($form['#attached']);
-    foreach ($commands as $command) {
-      $response->addCommand($command, TRUE);
-    }
-    return $response->addCommand(new ReplaceCommand(NULL, $output));
-  }
-
-  /**
    * Returns the progress status for a file upload process.
    *
    * @param string $key
diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php
index 40d0c31..5ac2c94 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -7,10 +7,14 @@
 
 namespace Drupal\file\Element;
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Url;
 use Drupal\file\Entity\File;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provides an AJAX/progress aware widget for uploading and saving a file.
@@ -124,6 +128,63 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
   }
 
   /**
+   * #ajax callback for managed_file upload forms.
+   *
+   * This ajax callback takes care about ensuring that there is no broken
+   * request due to too big files, as well as adding a class to the response
+   * that a new file got uploaded.
+   *
+   * @param array $form
+   *   The build form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The ajax response of the ajax upload.
+   */
+  public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
+    /** @var \Drupal\Core\Render\RendererInterface $renderer */
+    $renderer = \Drupal::service('renderer');
+
+    $form_parents = explode('/', $request->query->get('element_parents'));
+    $form_build_id = $request->query->get('form_build_id');
+    $request_form_build_id = $request->request->get('form_build_id');
+
+    if (empty($request_form_build_id) || $form_build_id !== $request_form_build_id) {
+      // Invalid request.
+      drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
+      $response = new AjaxResponse();
+      $status_messages = array('#type' => 'status_messages');
+      return $response->addCommand(new ReplaceCommand(NULL, $renderer->renderRoot($status_messages)));
+    }
+
+    // Retrieve the element to be rendered.
+    $form = NestedArray::getValue($form, $form_parents);
+
+
+    // Add the special Ajax class if a new file was added.
+    $current_file_count = $form_state->get('file_upload_delta_initial');
+    if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
+      $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+    }
+    // Otherwise just add the new content class on a placeholder.
+    else {
+      $form['#suffix'] .= '<span class="ajax-new-content"></span>';
+    }
+
+    $status_messages = array('#type' => 'status_messages');
+    $form['#prefix'] .= $renderer->renderRoot($status_messages);
+    $output = $renderer->renderRoot($form);
+
+    $response = new AjaxResponse();
+    $response->setAttachments($form['#attached']);
+
+    return $response->addCommand(new ReplaceCommand(NULL, $output));
+  }
+
+  /**
    * Render API callback: Expands the managed_file element type.
    *
    * Expands the file type to include Upload and Remove buttons, as well as
@@ -145,7 +206,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
     $element['#tree'] = TRUE;
 
     $ajax_settings = [
-      'url' => Url::fromRoute('file.ajax_upload'),
+      'callback' => [get_called_class(), 'uploadAjaxCallback'],
       'options' => [
         'query' => [
           'element_parents' => implode('/', $element['#array_parents']),
diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
index da48f74..08ea7b3 100644
--- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
+++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
@@ -407,7 +407,6 @@ public static function process($element, FormStateInterface $form_state, $form)
     // file, the entire group of file fields is updated together.
     if ($element['#cardinality'] != 1) {
       $parents = array_slice($element['#array_parents'], 0, -1);
-      $new_url = Url::fromRoute('file.ajax_upload');
       $new_options = array(
         'query' => array(
           'element_parents' => implode('/', $parents),
@@ -418,7 +417,6 @@ public static function process($element, FormStateInterface $form_state, $form)
       $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
       foreach (Element::children($element) as $key) {
         if (isset($element[$key]['#ajax'])) {
-          $element[$key]['#ajax']['url'] = $new_url->setOptions($new_options);
           $element[$key]['#ajax']['options'] = $new_options;
           $element[$key]['#ajax']['wrapper'] = $new_wrapper;
         }
@@ -451,6 +449,19 @@ public static function processMultiple($element, FormStateInterface $form_state,
     $element_children = Element::children($element, TRUE);
     $count = count($element_children);
 
+    // Count the amount of already uploaded files, in order to display new
+    // items in \Drupal\file\Element\ManagedFile::uploadAjaxCallback.
+    if (!$form_state->isRebuilding()) {
+      $count_items_before = 0;
+      foreach ($element_children as $children) {
+        if (!empty($element[$children]['#default_value']['fids'])) {
+          $count_items_before++;
+        }
+      }
+
+      $form_state->set('file_upload_delta_initial', $count_items_before);
+    }
+
     foreach ($element_children as $delta => $key) {
       if ($key != $element['#file_upload_delta']) {
         $description = static::getDescriptionFromElement($element[$key]);
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
index 0c29322..084847a 100644
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ b/core/modules/system/src/Controller/FormAjaxController.php
@@ -7,9 +7,8 @@
 
 namespace Drupal\system\Controller;
 
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\UpdateBuildIdCommand;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Render\MainContent\MainContentRendererInterface;
@@ -20,7 +19,6 @@
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\HttpKernel\Exception\HttpException;
 
 /**
  * Defines a controller to respond to form Ajax requests.
@@ -63,6 +61,13 @@ class FormAjaxController implements ContainerInjectionInterface {
   protected $routeMatch;
 
   /**
+   * The form AJAX response builder.
+   *
+   * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
+   */
+  protected $formAjaxResponseBuilder;
+
+  /**
    * Constructs a FormAjaxController object.
    *
    * @param \Psr\Log\LoggerInterface $logger
@@ -73,15 +78,18 @@ class FormAjaxController implements ContainerInjectionInterface {
    *   The renderer.
    * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
    *   The main content to AJAX Response renderer.
-   * @param \Drupal\Core\Routing\RouteMatchInterface
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The current route match.
+   * @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
+   *   The form AJAX response builder.
    */
-  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
+  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match, FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
     $this->logger = $logger;
     $this->formBuilder = $form_builder;
     $this->renderer = $renderer;
     $this->ajaxRenderer = $ajax_renderer;
     $this->routeMatch = $route_match;
+    $this->formAjaxResponseBuilder = $form_ajax_response_builder;
   }
 
   /**
@@ -93,7 +101,8 @@ public static function create(ContainerInterface $container) {
       $container->get('form_builder'),
       $container->get('renderer'),
       $container->get('main_content_renderer.ajax'),
-      $container->get('current_route_match')
+      $container->get('current_route_match'),
+      $container->get('form_ajax_response_builder')
     );
   }
 
@@ -121,38 +130,7 @@ public function content(Request $request) {
 
     $this->formBuilder->processForm($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.
-    $callback = NULL;
-    if ($triggering_element = $form_state->getTriggeringElement()) {
-      $callback = $triggering_element['#ajax']['callback'];
-    }
-    $callback = $form_state->prepareCallback($callback);
-    if (empty($callback) || !is_callable($callback)) {
-      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
-    }
-    $result = call_user_func_array($callback, [&$form, &$form_state]);
-
-    // If the callback is an #ajax callback, the result is a render array, and
-    // we need to turn it into an AJAX response, so that we can add any commands
-    // we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
-    // submit from a cached page.
-    if ($result instanceof AjaxResponse) {
-      $response = $result;
-    }
-    else {
-      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
-      $response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
-    }
-
-    foreach ($commands as $command) {
-      $response->addCommand($command, TRUE);
-    }
-    return $response;
+    return $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
   }
 
   /**
@@ -186,17 +164,6 @@ protected function getForm(Request $request) {
       throw new BadRequestHttpException();
     }
 
-    // When a page level cache is enabled, the form-build id might have been
-    // replaced from within \Drupal::formBuilder()->getCache(). If this is the
-    // case, it is also necessary to update it in the browser by issuing an
-    // appropriate Ajax command.
-    $commands = [];
-    if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
-      // If the form build ID has changed, issue an Ajax command to update it.
-      $commands[] = new UpdateBuildIdCommand($form['#build_id_old'], $form['#build_id']);
-      $form_build_id = $form['#build_id'];
-    }
-
     // Since some of the submit handlers are run, redirects need to be disabled.
     $form_state->disableRedirect();
 
@@ -213,7 +180,7 @@ protected function getForm(Request $request) {
     $form_state->setUserInput($request->request->all());
     $form_id = $form['#form_id'];
 
-    return new FileAjaxForm($form, $form_state, $form_id, $form_build_id, $commands);
+    return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], []);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
new file mode 100644
index 0000000..9fdac57
--- /dev/null
+++ b/core/modules/system/src/Tests/Ajax/AjaxFormCacheTest.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Ajax\AjaxFormCacheTest.
+ */
+
+namespace Drupal\system\Tests\Ajax;
+
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Url;
+
+/**
+ * Tests the usage of form caching for AJAX forms.
+ *
+ * @group Ajax
+ */
+class AjaxFormCacheTest extends AjaxTestBase {
+
+  /**
+   * Tests the usage of form cache for AJAX forms.
+   */
+  public function testFormCacheUsage() {
+    /** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
+    $key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
+    $this->drupalLogin($this->rootUser);
+
+    // Ensure that the cache is empty.
+    $this->assertEqual(0, count($key_value_expirable->getAll()));
+
+    // Visit an AJAX form that is not cached, 3 times.
+    $uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form');
+    $this->drupalGet($uncached_form_url);
+    $this->drupalGet($uncached_form_url);
+    $this->drupalGet($uncached_form_url);
+
+    // The number of cache entries should not have changed.
+    $this->assertEqual(0, count($key_value_expirable->getAll()));
+
+    // Visit a form that is explicitly cached, 3 times.
+    $cached_form_url = Url::fromRoute('ajax_forms_test.cached_form');
+    $this->drupalGet($cached_form_url);
+    $this->drupalGet($cached_form_url);
+    $this->drupalGet($cached_form_url);
+
+    // The number of cache entries should be exactly 3.
+    $this->assertEqual(3, count($key_value_expirable->getAll()));
+  }
+
+  /**
+   * Tests AJAX forms in blocks.
+   */
+  public function testBlockForms() {
+    $this->container->get('module_installer')->install(['block', 'search']);
+    $this->rebuildContainer();
+    $this->container->get('router.builder')->rebuild();
+    $this->drupalLogin($this->rootUser);
+
+    $this->drupalPlaceBlock('search_form_block', ['weight' => -5]);
+    $this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
+
+    $this->drupalGet('');
+    $this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
+    $this->assertOptionSelected('edit-test1--2', 'option1');
+    $this->assertOption('edit-test1--2', 'option3');
+  }
+
+  /**
+   * Tests AJAX forms on pages with a query string.
+   */
+  public function testQueryString() {
+    $this->container->get('module_installer')->install(['block']);
+    $this->drupalLogin($this->rootUser);
+
+    $this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
+
+    $url = Url::fromRoute('entity.user.canonical', ['user' => $this->rootUser->id()], ['query' => ['foo' => 'bar']]);
+    $this->drupalGet($url);
+    $this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
+    $url->setOption('query', [
+      'foo' => 'bar',
+      FormBuilderInterface::AJAX_FORM_REQUEST => 1,
+      MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
+    ]);
+    $this->assertUrl($url);
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
index c5dafbe..03727b6 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
+++ b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
@@ -54,9 +54,15 @@ public function testSimpleAJAXFormValue() {
    $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
 
    $edit = ['select' => 'red'];
-   $this->drupalPostAjaxForm(NULL, $edit, 'select');
+   $commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
    $build_id_second_ajax = $this->getFormBuildId();
-   $this->assertEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id remains the same on subsequent AJAX submissions');
+   $this->assertNotEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id changes on subsequent AJAX submissions');
+   $expected = [
+     'command' => 'update_build_id',
+     'old' => $build_id_first_ajax,
+     'new' => $build_id_second_ajax,
+   ];
+   $this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
 
    // Repeat the test sequence but this time with a page loaded from the cache.
    $this->drupalGet('ajax_forms_test_get_form');
@@ -77,9 +83,15 @@ public function testSimpleAJAXFormValue() {
    $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
 
    $edit = ['select' => 'red'];
-   $this->drupalPostAjaxForm(NULL, $edit, 'select');
+   $commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
    $build_id_from_cache_second_ajax = $this->getFormBuildId();
-   $this->assertEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id remains the same on subsequent AJAX submissions');
+   $this->assertNotEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions');
+   $expected = [
+     'command' => 'update_build_id',
+     'old' => $build_id_from_cache_first_ajax,
+     'new' => $build_id_from_cache_second_ajax,
+   ];
+   $this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
  }
 
   /**
diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php
index e290977..23c9403 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Tests\Ajax;
 
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Url;
 
 /**
@@ -146,11 +147,11 @@ public function testDialog() {
     $this->assertTrue($dialog_js_exists, 'Drupal dialog JS added to the page.');
 
     // Check that the response matches the expected value.
-    $this->assertEqual($modal_expected_response, $ajax_result[3], 'POST request modal dialog JSON response matches.');
+    $this->assertEqual($modal_expected_response, $ajax_result[4], 'POST request modal dialog JSON response matches.');
 
     // Abbreviated test for "normal" dialogs, testing only the difference.
     $ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(), 'button2');
-    $this->assertEqual($normal_expected_response, $ajax_result[3], 'POST request normal dialog JSON response matches.');
+    $this->assertEqual($normal_expected_response, $ajax_result[4], 'POST request normal dialog JSON response matches.');
 
     // Check that requesting a form dialog without JS goes to a page.
     $this->drupalGet('ajax-test/dialog-form');
@@ -165,7 +166,10 @@ public function testDialog() {
       'edit-preview' => [
         'callback' => '::preview',
         'event' => 'click',
-        'url' => Url::fromRoute('system.ajax')->toString(),
+        'url' => Url::fromRoute('ajax_test.dialog_form', [], ['query' => [
+            MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal',
+            FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
+          ]])->toString(),
         'dialogType' => 'ajax',
         'submit' => [
           '_triggering_element_name' => 'op',
diff --git a/core/modules/system/src/Tests/Ajax/MultiFormTest.php b/core/modules/system/src/Tests/Ajax/MultiFormTest.php
index 449861c..860deca 100644
--- a/core/modules/system/src/Tests/Ajax/MultiFormTest.php
+++ b/core/modules/system/src/Tests/Ajax/MultiFormTest.php
@@ -81,7 +81,7 @@ function testMultiForm() {
     // page update, ensure the same as above.
     foreach ($field_xpaths as $form_html_id => $field_xpath) {
       for ($i = 0; $i < 2; $i++) {
-        $this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_html_id);
+        $this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), NULL, array(), array(), $form_html_id);
         $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, 'Found the correct number of field items after an AJAX submission.');
         $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, 'Found the "add more" button after an AJAX submission.');
         $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
diff --git a/core/modules/system/src/Tests/Form/RebuildTest.php b/core/modules/system/src/Tests/Form/RebuildTest.php
index e993b49..53de2f2 100644
--- a/core/modules/system/src/Tests/Form/RebuildTest.php
+++ b/core/modules/system/src/Tests/Form/RebuildTest.php
@@ -96,7 +96,7 @@ function testPreserveFormActionAfterAJAX() {
     // submission and verify it worked by ensuring the updated page has two text
     // field items in the field for which we just added an item.
     $this->drupalGet('node/add/page');
-    $this->drupalPostAjaxForm(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'node-page-form');
+    $this->drupalPostAjaxForm(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), NULL, array(), array(), 'node-page-form');
     $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.');
 
     // Submit the form with the non-Ajax "Save" button, leaving the title field
@@ -113,6 +113,9 @@ function testPreserveFormActionAfterAJAX() {
 
     // Ensure that the form's action is correct.
     $forms = $this->xpath('//form[contains(@class, "node-page-form")]');
-    $this->assert(count($forms) == 1 && $forms[0]['action'] == Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), 'Re-rendered form contains the correct action value.');
+    $this->assertEqual(1, count($forms));
+    // Strip query params off the action before asserting.
+    $url = parse_url($forms[0]['action'])['path'];
+    $this->assertEqual(Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), $url);
   }
 }
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
index 4366426..3caf5ba 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.routing.yml
@@ -29,3 +29,11 @@ ajax_forms_test.lazy_load_form:
     _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestLazyLoadForm'
   requirements:
     _access: 'TRUE'
+
+ajax_forms_test.cached_form:
+  path: '/ajax_forms_test_cached_form'
+  defaults:
+    _title: 'AJAX forms cached form test'
+    _form: '\Drupal\ajax_forms_test\Form\AjaxFormsTestCachedForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php
new file mode 100644
index 0000000..3b22783
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCachedForm.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ajax_forms_test\Form\AjaxFormsTestCachedForm.
+ */
+
+namespace Drupal\ajax_forms_test\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides an AJAX form that will be cached.
+ */
+class AjaxFormsTestCachedForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_form_cache_test_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['test1'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Test 1'),
+      '#options' => [
+        'option1' => $this->t('Option 1'),
+        'option2' => $this->t('Option 2'),
+      ],
+      '#ajax' => [
+        'url' => Url::fromRoute('system.ajax'),
+      ],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+}
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php
new file mode 100644
index 0000000..a588378
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Plugin/Block/AjaxFormBlock.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ajax_forms_test\Plugin\Block\AjaxFormBlock.
+ */
+
+namespace Drupal\ajax_forms_test\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides an AJAX form block.
+ *
+ * @Block(
+ *   id = "ajax_forms_test_block",
+ *   admin_label = @Translation("AJAX test form"),
+ *   category = @Translation("Forms")
+ * )
+ */
+class AjaxFormBlock extends BlockBase implements FormInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * Constructs a new AjaxFormBlock.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->formBuilder = $form_builder;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('form_builder')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    return $this->formBuilder->getForm($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'ajax_forms_test_block';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['test1'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Test 1'),
+      '#required' => TRUE,
+      '#options' => [
+        'option1' => $this->t('Option 1'),
+        'option2' => $this->t('Option 2'),
+      ],
+      '#ajax' => [
+        'callback' => '::updateOptions',
+        'wrapper' => 'edit-test1-wrapper',
+      ],
+      '#prefix' => '<div id="edit-test1-wrapper">',
+      '#suffix' => '</div>',
+    ];
+    return $form;
+  }
+
+  /**
+   * Updates the options of a select list.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The updated form element.
+   */
+  public function updateOptions(array $form, FormStateInterface $form_state) {
+    $form['test1']['#options']['option1'] = $this->t('Option 1!!!');
+    $form['test1']['#options'] += [
+      'option3' => $this->t('Option 3'),
+      'option4' => $this->t('Option 4'),
+    ];
+    return $form['test1'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+  }
+
+}
diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc
index 64b9077..7e07366 100644
--- a/core/modules/views_ui/admin.inc
+++ b/core/modules/views_ui/admin.inc
@@ -51,6 +51,11 @@ function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_pa
   // Add the AJAX behavior to the triggering element.
   $triggering_element = &$wrapping_element[$trigger_key];
   $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form';
+
+  // Specify the #ajax URL in order to retain form caching.
+  // @todo Remove this in https://www.drupal.org/node/2500523.
+  $triggering_element['#ajax']['url'] = Url::fromRoute('system.ajax');
+
   // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID
   // for the AJAX wrapper, because it remembers IDs across AJAX requests (and
   // won't reuse them), but in our case we need to use the same ID from request
diff --git a/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php
new file mode 100644
index 0000000..eb01501
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\EventSubscriber\FormAjaxSubscriberTest.
+ */
+
+namespace Drupal\Tests\Core\Form\EventSubscriber;
+
+use Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber;
+use Drupal\Core\Form\FormAjaxException;
+use Drupal\Core\Form\FormState;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
+ * @group EventSubscriber
+ */
+class FormAjaxSubscriberTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber
+   */
+  protected $subscriber;
+
+  /**
+   * @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $formAjaxResponseBuilder;
+
+  /**
+   * @var \Symfony\Component\HttpKernel\HttpKernelInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $httpKernel;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+    $this->formAjaxResponseBuilder = $this->getMock('Drupal\Core\Form\FormAjaxResponseBuilderInterface');
+    $this->subscriber = new FormAjaxSubscriber($this->formAjaxResponseBuilder);
+  }
+
+  /**
+   * @covers ::onException
+   */
+  public function testOnException() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $expected_form = $form + [
+      '#build_id_old' => 'the_build_id',
+    ];
+    $form_state = new FormState();
+    $exception = new FormAjaxException($form, $form_state);
+
+    $request = new Request([], ['form_build_id' => 'the_build_id']);
+    $commands = [];
+    $response = new Response('');
+
+    $this->formAjaxResponseBuilder->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $expected_form, $form_state, $commands)
+      ->willReturn($response);
+
+    $event = $this->assertResponseFromException($request, $exception, $response);
+    $this->assertSame(200, $event->getResponse()->headers->get('X-Status-Code'));
+  }
+
+  /**
+   * @covers ::onException
+   */
+  public function testOnExceptionNewBuildId() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $expected_form = $form + [
+      '#build_id_old' => 'a_new_build_id',
+    ];
+    $form_state = new FormState();
+    $exception = new FormAjaxException($form, $form_state);
+
+    $request = new Request([], ['form_build_id' => 'a_new_build_id']);
+    $commands = [];
+    $response = new Response('');
+
+    $this->formAjaxResponseBuilder->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $expected_form, $form_state, $commands)
+      ->willReturn($response);
+
+    $event = $this->assertResponseFromException($request, $exception, $response);
+    $this->assertSame(200, $event->getResponse()->headers->get('X-Status-Code'));
+  }
+
+  /**
+   * @covers ::onException
+   */
+  public function testOnExceptionOtherClass() {
+    $request = new Request();
+    $exception = new \Exception();
+
+    $this->formAjaxResponseBuilder->expects($this->never())
+      ->method('buildResponse');
+
+    $this->assertResponseFromException($request, $exception, NULL);
+  }
+
+  /**
+   * @covers ::onException
+   */
+  public function testOnExceptionResponseBuilderException() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $expected_form = $form + [
+      '#build_id_old' => 'the_build_id',
+    ];
+    $form_state = new FormState();
+    $exception = new FormAjaxException($form, $form_state);
+    $request = new Request([], ['form_build_id' => 'the_build_id']);
+    $commands = [];
+
+    $expected_exception = new HttpException(500, 'The specified #ajax callback is empty or not callable.');
+    $this->formAjaxResponseBuilder->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $expected_form, $form_state, $commands)
+      ->willThrowException($expected_exception);
+
+    $event = $this->assertResponseFromException($request, $exception, NULL);
+    $this->assertSame($expected_exception, $event->getException());
+  }
+
+  /**
+   * @covers ::onException
+   * @covers ::getFormAjaxException
+   */
+  public function testOnExceptionNestedException() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $expected_form = $form + [
+        '#build_id_old' => 'the_build_id',
+      ];
+    $form_state = new FormState();
+    $form_exception = new FormAjaxException($form, $form_state);
+    $exception = new \Exception('', 0, $form_exception);
+
+    $request = new Request([], ['form_build_id' => 'the_build_id']);
+    $commands = [];
+    $response = new Response('');
+
+    $this->formAjaxResponseBuilder->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $expected_form, $form_state, $commands)
+      ->willReturn($response);
+
+    $this->assertResponseFromException($request, $exception, $response);
+  }
+
+  /**
+   * @covers ::getFormAjaxException
+   */
+  public function testOnExceptionNestedWrongException() {
+    $nested_exception = new \Exception();
+    $exception = new \Exception('', 0, $nested_exception);
+    $request = new Request();
+
+    $this->formAjaxResponseBuilder->expects($this->never())
+      ->method('buildResponse');
+
+    $this->assertResponseFromException($request, $exception, NULL);
+  }
+
+  /**
+   * Asserts that the expected response is derived from the given exception.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   * @param \Exception $exception
+   *   The exception to pass to the event.
+   * @param \Symfony\Component\HttpFoundation\Response|null $expected_response
+   *   The response expected to be set on the event.
+   *
+   * @return \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent
+   *   The event used to derive the response.
+   */
+  protected function assertResponseFromException(Request $request, \Exception $exception, $expected_response) {
+    $event = new GetResponseForExceptionEvent($this->httpKernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception);
+    $this->subscriber->onException($event);
+
+    $this->assertSame($expected_response, $event->getResponse());
+    return $event;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php
new file mode 100644
index 0000000..0698e28
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseBuilderTest.php
@@ -0,0 +1,212 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormAjaxResponseBuilderTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Form\FormAjaxResponseBuilder;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormAjaxResponseBuilder
+ * @group Form
+ */
+class FormAjaxResponseBuilderTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $renderer;
+
+  /**
+   * @var \Drupal\Core\Routing\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeMatch;
+
+  /**
+   * @var \Drupal\Core\Form\FormAjaxResponseBuilder
+   */
+  protected $formAjaxResponseBuilder;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->renderer = $this->getMock('Drupal\Core\Render\MainContent\MainContentRendererInterface');
+    $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $this->formAjaxResponseBuilder = new FormAjaxResponseBuilder($this->renderer, $this->routeMatch);
+  }
+
+  /**
+   * @covers ::buildResponse
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function testBuildResponseNoTriggeringElement() {
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $commands = [];
+
+    $expected = [];
+    $this->assertSame($expected, $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands));
+  }
+
+  /**
+   * @covers ::buildResponse
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function testBuildResponseNoCallable() {
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $triggering_element = [];
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $expected = [];
+    $this->assertSame($expected, $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands));
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseRenderArray() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return $form['test'];
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [
+      'test' => [
+        '#type' => 'textfield',
+      ],
+    ];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $this->renderer->expects($this->once())
+      ->method('renderResponse')
+      ->with($form['test'], $request, $this->routeMatch)
+      ->willReturn(new AjaxResponse([]));
+
+    $result = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands, $result->getCommands());
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseResponse() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return new AjaxResponse([]);
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $result = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands, $result->getCommands());
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseWithCommands() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return new AjaxResponse([]);
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [
+      'test' => [
+        '#type' => 'textfield',
+      ],
+    ];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [
+      new AlertCommand('alert!'),
+    ];
+    $commands_expected = [];
+    $commands_expected[] = ['command' => 'alert', 'text' => 'alert!'];
+
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $result = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands_expected, $result->getCommands());
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseWithUpdateCommand() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return new AjaxResponse([]);
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [
+      '#build_id' => 'the_build_id',
+      '#build_id_old' => 'a_new_build_id',
+      'test' => [
+        '#type' => 'textfield',
+      ],
+    ];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [
+      new AlertCommand('alert!'),
+    ];
+    $commands_expected = [];
+    $commands_expected[] = ['command' => 'update_build_id', 'old' => 'a_new_build_id', 'new' => 'the_build_id'];
+    $commands_expected[] = ['command' => 'alert', 'text' => 'alert!'];
+
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $result = $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands_expected, $result->getCommands());
+  }
+
+}
