diff --git a/core/core.services.yml b/core/core.services.yml
index 7b41a1d..3e00fcb 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -828,6 +828,11 @@ services:
     class: Drupal\Core\EventSubscriber\ContentControllerSubscriber
     tags:
       - { name: event_subscriber }
+  route_form_controller_subscriber:
+    class: Drupal\Core\EventSubscriber\FormControllerSubscriber
+    arguments: ['@form_ajax_response_handler']
+    tags:
+      - { name: event_subscriber }
   route_special_attributes_subscriber:
     class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
     tags:
@@ -874,6 +879,9 @@ services:
   controller.entity_form:
     class: Drupal\Core\Entity\HtmlEntityFormController
     arguments: ['@controller_resolver', '@form_builder', '@entity.manager']
+  form_ajax_response_handler:
+    class: Drupal\Core\Form\FormAjaxResponseHandler
+    arguments: ['@main_content_renderer.ajax', '@current_route_match']
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php
index 6a9b9d5..03a76d4 100644
--- a/core/lib/Drupal/Core/Controller/FormController.php
+++ b/core/lib/Drupal/Core/Controller/FormController.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Controller;
 
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -76,6 +77,12 @@ public function getContentResult(Request $request, RouteMatchInterface $route_ma
     unset($args[0], $args[1]);
     $form_state->addBuildInfo('args', array_values($args));
 
+    // If this form is being submitted back to its original URL, disable all
+    // redirects.
+    if ($request->query->get(FormControllerSubscriber::POST_TO_ORIGINAL_URL) === FormControllerSubscriber::DRUPAL_AJAX_POST) {
+      $form_state->disableRedirect();
+    }
+
     return $this->formBuilder->buildForm($form_object, $form_state);
   }
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
new file mode 100644
index 0000000..e5b6338
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\FormControllerSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Ajax\UpdateBuildIdCommand;
+use Drupal\Core\Form\FormAjaxException;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Wraps controllers that return forms for their response.
+ */
+class FormControllerSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Request key for a form that will post to original URL on AJAX submission.
+   */
+  const POST_TO_ORIGINAL_URL = 'post_to_original_url';
+
+  /**
+   * Request value for an AJAX form submission.
+   */
+  const DRUPAL_AJAX_POST = 'drupal_ajax_post';
+
+  /**
+   * The form AJAX handler.
+   *
+   * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface
+   */
+  protected $formAjaxResponseHandler;
+
+  /**
+   * Constructs a new FormControllerSubscriber.
+   *
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX handler.
+   */
+  public function __construct(FormAjaxResponseHandlerInterface $form_ajax_response_handler) {
+    $this->formAjaxResponseHandler = $form_ajax_response_handler;
+  }
+
+  /**
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   */
+  public function onException(GetResponseForExceptionEvent $event) {
+    if ($exception = $this->getFormAjaxException($event->getException())) {
+      $request = $event->getRequest();
+      $form = $exception->getForm();
+      $form_state = $exception->getFormState();
+      $form_build_id = $request->get('form_build_id');
+
+      $commands = [];
+      if ($form_build_id !== $form['#build_id']) {
+        // If the form build ID has changed, issue an Ajax command to update it.
+        $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']);
+      }
+      $response = $this->formAjaxResponseHandler->buildResponse($request, $form, $form_state, $commands);
+      // 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);
+    }
+  }
+
+  /**
+   * @param \Exception $e
+   *
+   * @return \Drupal\Core\Form\FormAjaxException|null
+   */
+  protected function getFormAjaxException(\Exception $e) {
+    while ($e) {
+      if ($e instanceof FormAjaxException) {
+        return $e;
+      }
+
+      $e = $e->getPrevious();
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  public static function getSubscribedEvents() {
+    // Run before exception.logger.
+    $events[KernelEvents::EXCEPTION] = ['onException', 51];
+
+    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..4a405f6
--- /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;
+
+/**
+ * @todo.
+ */
+class FormAjaxException extends \Exception {
+
+  /**
+   * The form to cache.
+   *
+   * @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/FormAjaxResponseHandler.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php
new file mode 100644
index 0000000..39919a3
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseHandler.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Render\MainContent\MainContentRendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Handles AJAX form responses.
+ *
+ * Given the current request, a form render array, its form state, and any AJAX
+ * commands to apply to the form, generate a response object.
+ */
+class FormAjaxResponseHandler implements FormAjaxResponseHandlerInterface {
+
+  /**
+   * 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 FormAjaxResponseHandler.
+   *
+   * @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) {
+    // 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()) && 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]);
+
+    // 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/FormAjaxResponseHandlerInterface.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php
new file mode 100644
index 0000000..7582d8c
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseHandlerInterface.
+ */
+
+namespace Drupal\Core\Form;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an interface for handling AJAX form responses.
+ */
+interface FormAjaxResponseHandlerInterface {
+
+  /**
+   * 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.
+   */
+  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..854a540 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -14,6 +14,7 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\CsrfTokenGenerator;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\ElementInfoManagerInterface;
@@ -244,6 +245,11 @@ public function buildForm($form_id, FormStateInterface &$form_state) {
       }
     }
 
+    // If this form should post to the original URL, disable all form redirects.
+    if ($post_to_original_url = $this->requestStack->getCurrentRequest()->query->get(FormControllerSubscriber::POST_TO_ORIGINAL_URL) === FormControllerSubscriber::DRUPAL_AJAX_POST) {
+      $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,13 @@ 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 to be posted to the original URL,
+    // interrupt form rendering and return.
+    // @see \Drupal\Core\EventSubscriber\FormControllerSubscriber::onException()
+    if ($post_to_original_url) {
+      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/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index c886fec..aaf2969 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Render\Element;
@@ -128,9 +130,6 @@ public static function preRenderGroup($element) {
    */
   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
     $element = static::preRenderAjaxForm($element);
-    if (!empty($element['#ajax_processed'])) {
-      $form_state->setCached();
-    }
     return $element;
   }
 
@@ -237,8 +236,12 @@ public static function preRenderAjaxForm($element) {
       // 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.
+      if (isset($settings['callback']) && !isset($settings['url'])) {
+        $settings['url'] = Url::fromRoute('<current>');
+        $settings['options']['query'][FormControllerSubscriber::POST_TO_ORIGINAL_URL] = FormControllerSubscriber::DRUPAL_AJAX_POST;
+      }
       $settings += array(
-        'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
+        'url' => NULL,
         'options' => array(),
         'dialogType' => 'ajax',
       );
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index 90176f8..2252033 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -10,7 +10,12 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\system\Controller\FormAjaxController;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@@ -21,6 +26,42 @@
 class FileWidgetAjaxController extends FormAjaxController {
 
   /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a FileWidgetAjaxController object.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX response handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_response_handler, RendererInterface $renderer) {
+    parent::__construct($logger, $form_builder, $form_ajax_response_handler);
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('logger.factory')->get('ajax'),
+      $container->get('form_builder'),
+      $container->get('form_ajax_response_handler'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
    * Processes AJAX file uploads and deletions.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
index 0c29322..ce64867 100644
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ b/core/modules/system/src/Controller/FormAjaxController.php
@@ -7,20 +7,16 @@
 
 namespace Drupal\system\Controller;
 
-use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\UpdateBuildIdCommand;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\Core\Render\MainContent\MainContentRendererInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\system\FileAjaxForm;
 use Psr\Log\LoggerInterface;
 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.
@@ -42,25 +38,11 @@ class FormAjaxController implements ContainerInjectionInterface {
   protected $formBuilder;
 
   /**
-   * The renderer.
+   * The form AJAX response handler.
    *
-   * @var \Drupal\Core\Render\RendererInterface
+   * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface
    */
-  protected $renderer;
-
-  /**
-   * 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;
+  protected $formAjaxResponseHandler;
 
   /**
    * Constructs a FormAjaxController object.
@@ -69,19 +51,13 @@ class FormAjaxController implements ContainerInjectionInterface {
    *   A logger instance.
    * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
    *   The form builder.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer.
-   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
-   *   The main content to AJAX Response renderer.
-   * @param \Drupal\Core\Routing\RouteMatchInterface
-   *   The current route match.
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX response handler.
    */
-  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
+  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_response_handler) {
     $this->logger = $logger;
     $this->formBuilder = $form_builder;
-    $this->renderer = $renderer;
-    $this->ajaxRenderer = $ajax_renderer;
-    $this->routeMatch = $route_match;
+    $this->formAjaxResponseHandler = $form_ajax_response_handler;
   }
 
   /**
@@ -91,9 +67,7 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('logger.factory')->get('ajax'),
       $container->get('form_builder'),
-      $container->get('renderer'),
-      $container->get('main_content_renderer.ajax'),
-      $container->get('current_route_match')
+      $container->get('form_ajax_response_handler')
     );
   }
 
@@ -121,38 +95,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->formAjaxResponseHandler->buildResponse($request, $form, $form_state, $commands);
   }
 
   /**
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..359eb6f 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Ajax;
 
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 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,7 @@ public function testDialog() {
       'edit-preview' => [
         'callback' => '::preview',
         'event' => 'click',
-        'url' => Url::fromRoute('system.ajax')->toString(),
+        'url' => Url::fromRoute('ajax_test.dialog_form')->setOption('query', [FormControllerSubscriber::POST_TO_ORIGINAL_URL => FormControllerSubscriber::DRUPAL_AJAX_POST])->toString(),
         'dialogType' => 'ajax',
         'submit' => [
           '_triggering_element_name' => 'op',
diff --git a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php
index 997e8c3..bc261c4 100644
--- a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php
+++ b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php
@@ -246,7 +246,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $style_form['style_plugin']['#default_value'] = static::getSelected($form_state, array('page', 'style', 'style_plugin'), 'default', $style_form['style_plugin']);
     // Changing this dropdown updates $form['displays']['page']['options'] via
     // AJAX.
-    views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'page', 'options'));
+    views_ui_add_ajax_trigger($style_form, $form_state, 'style_plugin', array('displays', 'page', 'options'));
 
     $this->buildFormStyle($form, $form_state, 'page');
     $form['displays']['page']['options']['items_per_page'] = array(
@@ -387,7 +387,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       $style_form['style_plugin']['#default_value'] = static::getSelected($form_state, array('block', 'style', 'style_plugin'), 'default', $style_form['style_plugin']);
       // Changing this dropdown updates $form['displays']['block']['options']
       // via AJAX.
-      views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'block', 'options'));
+      views_ui_add_ajax_trigger($style_form, $form_state, 'style_plugin', array('displays', 'block', 'options'));
 
       $this->buildFormStyle($form, $form_state, 'block');
       $form['displays']['block']['options']['items_per_page'] = array(
@@ -554,7 +554,7 @@ protected function buildFormStyle(array &$form, FormStateInterface $form_state,
       $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options);
       $style_form['row_plugin']['#default_value'] = static::getSelected($form_state, array($type, 'style', 'row_plugin'), $default_value, $style_form['row_plugin']);
       // Changing this dropdown updates the individual row options via AJAX.
-      views_ui_add_ajax_trigger($style_form, 'row_plugin', array('displays', $type, 'options', 'style', 'row_options'));
+      views_ui_add_ajax_trigger($style_form, $form_state, 'row_plugin', array('displays', $type, 'options', 'style', 'row_options'));
 
       // This is the region that can be updated by AJAX. The base class doesn't
       // add anything here, but child classes can.
@@ -606,7 +606,7 @@ protected function buildFilters(&$form, FormStateInterface $form_state) {
       // Changing this dropdown updates the entire content of $form['displays']
       // via AJAX, since each bundle might have entirely different fields
       // attached to it, etc.
-      views_ui_add_ajax_trigger($form['displays']['show'], 'type', array('displays'));
+      views_ui_add_ajax_trigger($form['displays']['show'], $form_state, 'type', array('displays'));
     }
   }
 
diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc
index 64b9077..8f2cd58 100644
--- a/core/modules/views_ui/admin.inc
+++ b/core/modules/views_ui/admin.inc
@@ -34,6 +34,8 @@
  *   $form['some_wrapper']['triggering_element'] represents the element which
  *   will trigger the AJAX behavior, you would pass $form['some_wrapper'] for
  *   this parameter.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The current state of the form.
  * @param $trigger_key
  *   The key within the wrapping element that identifies which of its children
  *   serves as the AJAX trigger. In the above example, you would pass
@@ -44,13 +46,18 @@
  *   cause $form['dynamic_content']['section'] to be refreshed, you would pass
  *   array('dynamic_content', 'section') for this parameter.
  */
-function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) {
+function views_ui_add_ajax_trigger(&$wrapping_element, FormStateInterface $form_state, $trigger_key, $refresh_parents) {
   $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array());
   $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array());
 
   // 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 and retain form caching.
+  $triggering_element['#ajax']['url'] = Url::fromRoute('system.ajax');
+  $form_state->setCached();
+
   // 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/modules/views_ui/src/ViewAddForm.php b/core/modules/views_ui/src/ViewAddForm.php
index 9750094..d82570b 100644
--- a/core/modules/views_ui/src/ViewAddForm.php
+++ b/core/modules/views_ui/src/ViewAddForm.php
@@ -132,7 +132,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $show_form['wizard_key']['#default_value'] = WizardPluginBase::getSelected($form_state, array('show', 'wizard_key'), $default_value, $show_form['wizard_key']);
     // Changing this dropdown updates the entire content of $form['displays'] via
     // AJAX.
-    views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays'));
+    views_ui_add_ajax_trigger($show_form, $form_state, 'wizard_key', array('displays'));
 
     // Build the rest of the form based on the currently selected wizard plugin.
     $wizard_key = $show_form['wizard_key']['#default_value'];
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php
new file mode 100644
index 0000000..44d3e51
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\EventSubscriber\FormControllerSubscriberTest.
+ */
+
+namespace Drupal\Tests\Core\EventSubscriber;
+
+use Drupal\Core\Ajax\UpdateBuildIdCommand;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
+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\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * @coversDefaultClass \Drupal\Core\EventSubscriber\FormControllerSubscriber
+ * @group EventSubscriber
+ */
+class FormControllerSubscriberTest extends UnitTestCase {
+
+  /**
+   * @covers ::onException
+   */
+  public function testOnException() {
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $form_state = new FormState();
+    $request = new Request([FormControllerSubscriber::POST_TO_ORIGINAL_URL => FormControllerSubscriber::DRUPAL_AJAX_POST], ['form_build_id' => 'a_new_build_id']);
+    $exception = new FormAjaxException($form, $form_state);
+    $commands = [];
+    $commands[] = new UpdateBuildIdCommand('a_new_build_id', 'the_build_id');
+
+    $response = new Response('');
+    $form_ajax_response_handler->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $form, $form_state, $commands)
+      ->willReturn($response);
+
+    $controller = NULL;
+    $expected = $response;
+    $event = new GetResponseForExceptionEvent($http_kernel, $request, HttpKernelInterface::MASTER_REQUEST, $exception);
+    $subscriber->onException($event);
+    $this->assertSame($expected, $event->getResponse());
+    $this->assertSame(200, $event->getResponse()->headers->get('X-Status-Code'));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php
new file mode 100644
index 0000000..3568dfe
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormAjaxResponseHandlerTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Form\FormAjaxResponseHandler;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormAjaxResponseHandler
+ * @group Form
+ */
+class FormAjaxResponseHandlerTest 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\FormAjaxResponseHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->renderer = $this->getMock('Drupal\Core\Render\MainContent\MainContentRendererInterface');
+    $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $this->handler = new FormAjaxResponseHandler($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->handler->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->handler->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->handler->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->handler->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->handler->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands_expected, $result->getCommands());
+  }
+
+}
