diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php
index 11977a8662..d0e8cfccfa 100644
--- a/src/wp-admin/includes/file.php
+++ b/src/wp-admin/includes/file.php
@@ -989,16 +989,22 @@ function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
  *
  * @param string $upload_ref    The upload reference sent by the client.
  * @param int    $attachment_id Attachment post ID.
- * @return bool Whether the transient was set.
+ * @return true|WP_Error Whether the transient was set.
  */
 function _wp_set_upload_ref( $upload_ref, $attachment_id ) {
 	$upload_ref = preg_replace( '/[^a-zA-Z0-9_]/', '', $upload_ref );
 
-	if ( ! empty( $upload_ref ) ) {
-		return set_transient( '_wp_temp_image_ref:' . $upload_ref, $attachment_id, HOUR_IN_SECONDS );
+	if ( ! $upload_ref ) {
+		return new WP_Error( 'invalid_upload_ref', __( 'The upload reference may only contain alpha numeric characters.' ) );
 	}
 
-	return false;
+	$set = set_transient( '_wp_temp_image_ref:' . $upload_ref, $attachment_id, HOUR_IN_SECONDS );
+
+	if ( ! $set ) {
+		return new WP_Error( 'db_update_error', __( 'Failed to save upload reference.' ) );
+	}
+
+	return true;
 }
 
 /**
diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php
index 745a1ec314..33e5a230d0 100644
--- a/src/wp-admin/includes/media.php
+++ b/src/wp-admin/includes/media.php
@@ -420,7 +420,7 @@ function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrid
 
 		// At this point the image is uploaded successfully even if there were specific errors or some sub-sizes were not created.
 		// The transient is not needed any more.
-		if ( $_ref ) {
+		if ( true === $_ref ) {
 			_wp_clear_upload_ref( $_POST['_wp_temp_upload_ref'] );
 		}
 	}
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
index c1be06a2a0..6ae7ecd5ff 100644
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
@@ -100,6 +100,104 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
 			return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
 		}
 
+		$retry_ref = $request->get_header( 'X-WP-RetryUploadRef' );
+
+		if ( $retry_ref ) {
+			// Include upload ref functions.
+			require_once ABSPATH . 'wp-admin/includes/file.php';
+			$id = _wp_get_upload_ref_attachment_id( $retry_ref );
+
+			if ( ! $id ) {
+				return new WP_Error( 'upload_reference_not_found', __( 'Upload failed. Please try again.' ), array( 'status' => 500 ) );
+			}
+
+			$is_retry = true;
+		} else {
+			$id_and_file = $this->insert_attachment( $request );
+
+			if ( is_wp_error( $id_and_file ) ) {
+				return $id_and_file;
+			}
+
+			list( $id, $file ) = $id_and_file;
+			$is_retry          = false;
+		}
+
+		if ( is_wp_error( $id ) ) {
+			return $id;
+		}
+
+		$attachment = get_post( $id );
+
+		/**
+		 * Fires after a single attachment is created or updated via the REST API.
+		 *
+		 * @since 4.7.0
+		 *
+		 * @param WP_Post         $attachment Inserted or updated attachment
+		 *                                    object.
+		 * @param WP_REST_Request $request    The request sent to the API.
+		 * @param bool            $creating   True when creating an attachment, false when updating.
+		 */
+		do_action( 'rest_insert_attachment', $attachment, $request, true );
+
+		// Include admin function to get access to wp_generate_attachment_metadata().
+		require_once ABSPATH . 'wp-admin/includes/media.php';
+
+		if ( $is_retry ) {
+			wp_update_image_subsizes( $id );
+			_wp_clear_upload_ref( $retry_ref );
+		} else {
+			wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
+
+			if ( $request->get_header( 'X-WP-UploadRef' ) ) {
+				// Include upload ref functions.
+				require_once ABSPATH . 'wp-admin/includes/file.php';
+				_wp_clear_upload_ref( $request->get_header( 'X-WP-UploadRef' ) );
+			}
+		}
+
+		if ( isset( $request['alt_text'] ) ) {
+			update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
+		}
+
+		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
+
+		if ( is_wp_error( $fields_update ) ) {
+			return $fields_update;
+		}
+
+		$request->set_param( 'context', 'edit' );
+
+		/**
+		 * Fires after a single attachment is completely created or updated via the REST API.
+		 *
+		 * @since 5.0.0
+		 *
+		 * @param WP_Post         $attachment Inserted or updated attachment object.
+		 * @param WP_REST_Request $request    Request object.
+		 * @param bool            $creating   True when creating an attachment, false when updating.
+		 */
+		do_action( 'rest_after_insert_attachment', $attachment, $request, true );
+
+		$response = $this->prepare_item_for_response( $attachment, $request );
+		$response = rest_ensure_response( $response );
+		$response->set_status( 201 );
+		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
+
+		return $response;
+	}
+
+	/**
+	 * Inserts the attachment in the database.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @param WP_REST_Request $request
+	 *
+	 * @return array|WP_Error
+	 */
+	protected function insert_attachment( $request ) {
 		// Get the file via $_FILES or raw data.
 		$files   = $request->get_file_params();
 		$headers = $request->get_headers();
@@ -158,54 +256,22 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
 			return $id;
 		}
 
-		$attachment = get_post( $id );
-
-		/**
-		 * Fires after a single attachment is created or updated via the REST API.
-		 *
-		 * @since 4.7.0
-		 *
-		 * @param WP_Post         $attachment Inserted or updated attachment
-		 *                                    object.
-		 * @param WP_REST_Request $request    The request sent to the API.
-		 * @param bool            $creating   True when creating an attachment, false when updating.
-		 */
-		do_action( 'rest_insert_attachment', $attachment, $request, true );
-
-		// Include admin function to get access to wp_generate_attachment_metadata().
-		require_once ABSPATH . 'wp-admin/includes/media.php';
-
-		wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
-
-		if ( isset( $request['alt_text'] ) ) {
-			update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
-		}
+		if ( $request->get_header( 'X-WP-UploadRef' ) ) {
+			if ( ! wp_attachment_is_image( $id ) ) {
+				return new WP_Error( 'invalid_upload_ref', __( 'Upload references can only be used for image attachments.' ), array( 'status' => 400 ) );
+			}
 
-		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
+			// Include upload ref functions.
+			require_once ABSPATH . 'wp-admin/includes/file.php';
+			$set_ref = _wp_set_upload_ref( $request->get_header( 'X-WP-UploadRef' ), $id );
 
-		if ( is_wp_error( $fields_update ) ) {
-			return $fields_update;
+			if ( is_wp_error( $set_ref ) ) {
+				// TODO: cleanup attachment data.
+				return $set_ref;
+			}
 		}
 
-		$request->set_param( 'context', 'edit' );
-
-		/**
-		 * Fires after a single attachment is completely created or updated via the REST API.
-		 *
-		 * @since 5.0.0
-		 *
-		 * @param WP_Post         $attachment Inserted or updated attachment object.
-		 * @param WP_REST_Request $request    Request object.
-		 * @param bool            $creating   True when creating an attachment, false when updating.
-		 */
-		do_action( 'rest_after_insert_attachment', $attachment, $request, true );
-
-		$response = $this->prepare_item_for_response( $attachment, $request );
-		$response = rest_ensure_response( $response );
-		$response->set_status( 201 );
-		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
-
-		return $response;
+		return array( $id, $file );
 	}
 
 	/**
