Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.28
diff -u -p -r1.28 ajax.inc
--- includes/ajax.inc	26 Mar 2010 18:58:12 -0000	1.28
+++ includes/ajax.inc	31 Mar 2010 16:26:47 -0000
@@ -270,7 +270,7 @@ function ajax_form_callback() {
 
   // This call recreates the form relying solely on the $form_state that
   // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
+  $form = drupal_rebuild_form($form_id, $form_state, $form);
 
   // As part of drupal_process_form(), the element that triggered the form
   // submission is determined, and in the case of AJAX, it might not be a
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.446
diff -u -p -r1.446 form.inc
--- includes/form.inc	30 Mar 2010 07:05:58 -0000	1.446
+++ includes/form.inc	31 Mar 2010 16:26:47 -0000
@@ -306,14 +306,18 @@ function form_state_defaults() {
  *   may be found in node_forms(), search_forms(), and user_forms().
  * @param $form_state
  *   A keyed array containing the current state of the form.
- * @param $form_build_id
- *   If the AHAH callback calling this function only alters part of the form,
- *   then pass in the existing form_build_id so we can re-cache with the same
- *   csid.
+ * @param $old_form
+ *   If called from a context where some of the form properties of the
+ *   previously built form need to be retained in the newly built form, then
+ *   the $form variable from before this function is called can be passed as
+ *   this parameter. An example where this is used is from AJAX callbacks, where
+ *   the #build_id and #action of the newly built form need to be the same as in
+ *   the previously built form. This parameter should not be passed during a
+ *   "normal" form rebuild, where all of $form should be rebuilt fresh. 
  * @return
  *   The newly built form.
  */
-function drupal_rebuild_form($form_id, &$form_state, $form_build_id = NULL) {
+function drupal_rebuild_form($form_id, &$form_state, $old_form = NULL) {
   // AJAX and other contexts may call drupal_rebuild_form() even when
   // $form_state['rebuild'] isn't set, but _form_builder_handle_input_element()
   // needs to distinguish a rebuild from an initial build in order to process
@@ -323,17 +327,28 @@ function drupal_rebuild_form($form_id, &
 
   $form = drupal_retrieve_form($form_id, $form_state);
 
-  if (!isset($form_build_id)) {
-    // We need a new build_id for the new version of the form.
-    $form_build_id = 'form-' . md5(mt_rand());
+  // During a context where only part of the form will be returned to the
+  // browser (for example, AJAX or a RIA client), we reuse the old #build_id
+  // so as not to require client-side code to update the hidden 'build_id' input
+  // variable with a new value. Otherwise, we generate a new #build_id so as
+  // not to clobber the prior build's data in the form cache, allowing the user
+  // to use the browser's Back button to return to an earlier build, make
+  // changes, and resubmit.
+  $form['#build_id'] = isset($old_form['#build_id']) ? $old_form['#build_id'] : 'form-' . md5(mt_rand());
+
+  // A form's #action property defaults to request_uri(), but there may be cases
+  // (for example, AJAX), where the form is submitted to an alternate URL but we
+  // want the rebuilt form to retain the #action of the original form.
+  if (isset($old_form['#action'])) {
+    $form['#action'] = $old_form['#action'];
   }
-  $form['#build_id'] = $form_build_id;
+
   drupal_prepare_form($form_id, $form, $form_state);
 
   if (empty($form_state['no_cache'])) {
     // We cache the form structure and the form state so it can be retrieved
     // later for validation.
-    form_set_cache($form_build_id, $form, $form_state);
+    form_set_cache($form['#build_id'], $form, $form_state);
   }
 
   // Clear out all group associations as these might be different when
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.23
diff -u -p -r1.23 file.module
--- modules/file/file.module	27 Mar 2010 05:52:49 -0000	1.23
+++ modules/file/file.module	31 Mar 2010 16:26:48 -0000
@@ -239,7 +239,7 @@ function file_ajax_upload() {
 
   // This call recreates the form relying solely on the form_state that the
   // drupal_process_form() set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
+  $form = drupal_rebuild_form($form_id, $form_state, $form);
 
   // Retrieve the element to be rendered.
   foreach ($form_parents as $parent) {
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.43
diff -u -p -r1.43 form.test
--- modules/simpletest/tests/form.test	27 Mar 2010 05:52:50 -0000	1.43
+++ modules/simpletest/tests/form.test	31 Mar 2010 16:26:49 -0000
@@ -783,6 +783,65 @@ class FormsRebuildTestCase extends Drupa
     $this->assertNoFieldChecked('edit-checkbox-2-default-off', t('A newly added checkbox was initialized with a default unchecked state.'));
     $this->assertFieldById('edit-text-2', 'DEFAULT 2', t('A newly added textfield was initialized with its default value.'));
   }
+
+  /**
+   * Test an AJAX submission followed by a non-AJAX submission that triggers a validation error.
+   *
+   * Ensure that the form returned after the validation error contains the same
+   * 'action' attribute as the original form, and not the AJAX url.
+   */
+  function testPreserveFormActionAfterAJAX() {
+    // Create a multi-valued field for 'page' nodes to use for AJAX testing.
+    $field_name = 'field_ajax_test';
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'text',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+    );
+    field_create_field($field);
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => 'page',
+    );
+    field_create_instance($instance);
+
+    // Login a user who can create 'page' nodes.
+    $this->web_user = $this->drupalCreateUser(array('create page content'));
+    $this->drupalLogin($this->web_user);
+
+    // Get the form for adding a 'page' node. Save the content in a local
+    // variable, because drupalPostAJAX() will replace $this->content.
+    $this->drupalGet('node/add/page');
+    $content = $this->content;
+
+    // Submit an "add another item" AJAX submission and verify it worked by
+    // ensuring it returned two text fields.
+    $commands = $this->drupalPostAJAX(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')));
+    $fragment = simplexml_load_string('<div>' . $commands[1]['data'] . '</div>');
+    $this->assert(count($fragment->xpath('//input[@type="text"]')) == 2, t('AJAX submission succeeded.'));
+
+    // Submit the form with the non-AJAX "Save" button. Leave the title field
+    // blank to trigger a validation error. First restore $this->content,
+    // because drupalPost() needs that to contain the form, not the JSON string
+    // left by drupalPostAJAX().
+    // @todo While not necessary for this test, we would be emulating the
+    //   browser better by calling drupalPost() with the AJAX-modified content
+    //   rather than with the original content from the drupalGet(), but that's
+    //   not possible with the current implementation of drupalPostAJAX(). See
+    //   http://drupal.org/node/384992.
+    $this->drupalSetContent($content);
+    $this->drupalPost(NULL, array(), t('Save'));
+
+    // Ensure that a validation error occurred since this test is for testing
+    // what happens to the form action after a validation error.
+    $this->assertText('Title field is required.', t('Non-AJAX submission correctly triggered a validation error.'));
+
+    // Ensure that the form's action is correct. There might be multiple forms
+    // on the page, so make sure to check the action attribute of the intended
+    // form.
+    $this->assertFieldByXPath('//form[@id="page-node-form" and @action="' . url('node/add/page') . '"]', NULL, t('Re-rendered form contains the correct action value.'));
+  }
 }
 
 /**
