<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[49252] trunk: REST API: Introduce support for batching API requests.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/49252">49252</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/49252","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>TimothyBlynJacobs</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2020-10-20 19:08:48 +0000 (Tue, 20 Oct 2020)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>REST API: Introduce support for batching API requests.

A new route is introduced, `batch/v1`, that accepts a list of API requests to run. Each request runs in sequence, and the responses are returned in the order they've been received.

Optionally, the `require-all-validate` validation mode can be used to first validate each request's parameters and only proceed with processing if each request validates successfully.

By default, the batch size is limited to 25 requests. This can be controlled using the `rest_get_max_batch_size` filter. Clients are strongly encouraged to discover the maximum batch size supported by the server by making an OPTIONS request to the `batch/v1` endpoint and inspecting the described arguments.

Additionally, the two new methods, `match_request_to_handler` and `respond_to_request` introduced in <a href="https://core.trac.wordpress.org/changeset/48947">[48947]</a> now have a `protected` visibility as we don't want to expose the inner workings of the `WP_REST_Server::dispatch` API.

Batching is not currently supported for GET requests.

Fixes <a href="https://core.trac.wordpress.org/ticket/50244">#50244</a>.
Props andraganescu, zieladam, TimothyBlynJacobs.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapiclasswprestserverphp">trunk/src/wp-includes/rest-api/class-wp-rest-server.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestserverphp">trunk/tests/phpunit/tests/rest-api/rest-server.php</a></li>
<li><a href="#trunktestsqunitfixtureswpapigeneratedjs">trunk/tests/qunit/fixtures/wp-api-generated.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesrestapiclasswprestserverphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/class-wp-rest-server.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/class-wp-rest-server.php   2020-10-20 19:05:51 UTC (rev 49251)
+++ trunk/src/wp-includes/rest-api/class-wp-rest-server.php     2020-10-20 19:08:48 UTC (rev 49252)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -94,7 +94,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct() {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->endpoints = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        // Meta endpoints.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        '/' => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 '/'         => array(
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'callback' => array( $this, 'get_index' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'methods'  => 'GET',
</span><span class="cx" style="display: block; padding: 0 10px">                                'args'     => array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -103,6 +103,51 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                                ),
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '/batch/v1' => array(
+                               'callback' => array( $this, 'serve_batch_request_v1' ),
+                               'methods'  => 'POST',
+                               'args'     => array(
+                                       'validation' => array(
+                                               'type'    => 'string',
+                                               'enum'    => array( 'require-all-validate', 'normal' ),
+                                               'default' => 'normal',
+                                       ),
+                                       'requests'   => array(
+                                               'required' => true,
+                                               'type'     => 'array',
+                                               'maxItems' => $this->get_max_batch_size(),
+                                               'items'    => array(
+                                                       'type'       => 'object',
+                                                       'properties' => array(
+                                                               'method'  => array(
+                                                                       'type'    => 'string',
+                                                                       'enum'    => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
+                                                                       'default' => 'POST',
+                                                               ),
+                                                               'path'    => array(
+                                                                       'type'     => 'string',
+                                                                       'required' => true,
+                                                               ),
+                                                               'body'    => array(
+                                                                       'type'                 => 'object',
+                                                                       'properties'           => array(),
+                                                                       'additionalProperties' => true,
+                                                               ),
+                                                               'headers' => array(
+                                                                       'type'                 => 'object',
+                                                                       'properties'           => array(),
+                                                                       'additionalProperties' => array(
+                                                                               'type'  => array( 'string', 'array' ),
+                                                                               'items' => array(
+                                                                                       'type' => 'string',
+                                                                               ),
+                                                                       ),
+                                                               ),
+                                                       ),
+                                               ),
+                                       ),
+                               ),
+                       ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -971,7 +1016,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @param WP_REST_Request $request The request object.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function match_request_to_handler( $request ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function match_request_to_handler( $request ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $method = $request->get_method();
</span><span class="cx" style="display: block; padding: 0 10px">                $path   = $request->get_route();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1058,7 +1103,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return WP_REST_Response
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function respond_to_request( $request, $route, $handler, $response ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function respond_to_request( $request, $route, $handler, $response ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Filters the response before executing any REST API callbacks.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1397,6 +1442,178 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Gets the maximum number of requests that can be included in a batch.
+        *
+        * @since 5.6.0
+        *
+        * @return int The maximum requests.
+        */
+       protected function get_max_batch_size() {
+               /**
+                * Filters the maximum number of requests that can be included in a batch.
+                *
+                * @param int $max_size The maximum size.
+                */
+               return apply_filters( 'rest_get_max_batch_size', 25 );
+       }
+
+       /**
+        * Serves the batch/v1 request.
+        *
+        * @since 5.6.0
+        *
+        * @param WP_REST_Request $batch_request The batch request object.
+        * @return WP_REST_Response The generated response object.
+        */
+       public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
+               $requests = array();
+
+               foreach ( $batch_request['requests'] as $args ) {
+                       $parsed_url = wp_parse_url( $args['path'] );
+
+                       if ( false === $parsed_url ) {
+                               $requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );
+
+                               continue;
+                       }
+
+                       $single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] );
+
+                       if ( ! empty( $parsed_url['query'] ) ) {
+                               $query_args = null; // Satisfy linter.
+                               wp_parse_str( $parsed_url['query'], $query_args );
+                               $single_request->set_query_params( $query_args );
+                       }
+
+                       if ( ! empty( $args['body'] ) ) {
+                               $single_request->set_body_params( $args['body'] );
+                       }
+
+                       if ( ! empty( $args['headers'] ) ) {
+                               $single_request->set_headers( $args['headers'] );
+                       }
+
+                       $requests[] = $single_request;
+               }
+
+               $matches    = array();
+               $validation = array();
+               $has_error  = false;
+
+               foreach ( $requests as $single_request ) {
+                       $match     = $this->match_request_to_handler( $single_request );
+                       $matches[] = $match;
+                       $error     = null;
+
+                       if ( is_wp_error( $match ) ) {
+                               $error = $match;
+                       }
+
+                       if ( ! $error ) {
+                               list( $route, $handler ) = $match;
+
+                               if ( isset( $handler['allow_batch'] ) ) {
+                                       $allow_batch = $handler['allow_batch'];
+                               } else {
+                                       $route_options = $this->get_route_options( $route );
+                                       $allow_batch   = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false;
+                               }
+
+                               if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
+                                       $error = new WP_Error(
+                                               'rest_batch_not_allowed',
+                                               __( 'The requested route does not support batch requests.' ),
+                                               array( 'status' => 400 )
+                                       );
+                               }
+                       }
+
+                       if ( ! $error ) {
+                               $check_required = $single_request->has_valid_params();
+                               if ( is_wp_error( $check_required ) ) {
+                                       $error = $check_required;
+                               }
+                       }
+
+                       if ( ! $error ) {
+                               $check_sanitized = $single_request->sanitize_params();
+                               if ( is_wp_error( $check_sanitized ) ) {
+                                       $error = $check_sanitized;
+                               }
+                       }
+
+                       if ( $error ) {
+                               $has_error    = true;
+                               $validation[] = $error;
+                       } else {
+                               $validation[] = true;
+                       }
+               }
+
+               $responses = array();
+
+               if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
+                       foreach ( $validation as $valid ) {
+                               if ( is_wp_error( $valid ) ) {
+                                       $responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
+                               } else {
+                                       $responses[] = null;
+                               }
+                       }
+
+                       return new WP_REST_Response(
+                               array(
+                                       'failed'    => 'validation',
+                                       'responses' => $responses,
+                               ),
+                               WP_Http::MULTI_STATUS
+                       );
+               }
+
+               foreach ( $requests as $i => $single_request ) {
+                       $clean_request = clone $single_request;
+                       $clean_request->set_url_params( array() );
+                       $clean_request->set_attributes( array() );
+                       $clean_request->set_default_params( array() );
+
+                       /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+                       $result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );
+
+                       if ( empty( $result ) ) {
+                               $match = $matches[ $i ];
+                               $error = null;
+
+                               if ( is_wp_error( $validation[ $i ] ) ) {
+                                       $error = $validation[ $i ];
+                               }
+
+                               if ( is_wp_error( $match ) ) {
+                                       $result = $this->error_to_response( $match );
+                               } else {
+                                       list( $route, $handler ) = $match;
+
+                                       if ( ! $error && ! is_callable( $handler['callback'] ) ) {
+                                               $error = new WP_Error(
+                                                       'rest_invalid_handler',
+                                                       __( 'The handler for the route is invalid' ),
+                                                       array( 'status' => 500 )
+                                               );
+                                       }
+
+                                       $result = $this->respond_to_request( $single_request, $route, $handler, $error );
+                               }
+                       }
+
+                       /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+                       $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );
+
+                       $responses[] = $this->envelope_response( $result, false )->get_data();
+               }
+
+               return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Sends an HTTP status code.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.4.0
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestserverphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-server.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-server.php        2020-10-20 19:05:51 UTC (rev 49251)
+++ trunk/tests/phpunit/tests/rest-api/rest-server.php  2020-10-20 19:08:48 UTC (rev 49252)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1617,6 +1617,320 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'rest_invalid_param', $events[0]['args'][0]->get_error_code() );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * @ticket       50244
+        * @dataProvider data_batch_v1_optin
+        */
+       public function test_batch_v1_optin( $allow_batch, $allowed ) {
+               $args = array(
+                       'methods'             => 'POST',
+                       'callback'            => static function () {
+                               return new WP_REST_Response( 'data' );
+                       },
+                       'permission_callback' => '__return_true',
+               );
+
+               if ( null !== $allow_batch ) {
+                       $args['allow_batch'] = $allow_batch;
+               }
+
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       $args
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'requests' => array(
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_do_request( $request );
+
+               $this->assertEquals( 207, $response->get_status() );
+
+               if ( $allowed ) {
+                       $this->assertEquals( 'data', $response->get_data()['responses'][0]['body'] );
+               } else {
+                       $this->assertEquals( 'rest_batch_not_allowed', $response->get_data()['responses'][0]['body']['code'] );
+               }
+       }
+
+       public function data_batch_v1_optin() {
+               return array(
+                       'missing'             => array( null, false ),
+                       'invalid type'        => array( true, false ),
+                       'invalid type string' => array( 'v1', false ),
+                       'wrong version'       => array( array( 'version1' => true ), false ),
+                       'false version'       => array( array( 'v1' => false ), false ),
+                       'valid'               => array( array( 'v1' => true ), true ),
+               );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_batch_v1_pre_validation() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'methods'             => 'POST',
+                               'callback'            => static function ( $request ) {
+                                       $project = $request['project'];
+                                       update_option( 'test_project', $project );
+
+                                       return new WP_REST_Response( $project );
+                               },
+                               'permission_callback' => '__return_true',
+                               'allow_batch'         => array( 'v1' => true ),
+                               'args'                => array(
+                                       'project' => array(
+                                               'type' => 'string',
+                                               'enum' => array( 'gutenberg', 'WordPress' ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'validation' => 'require-all-validate',
+                               'requests'   => array(
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'gutenberg',
+                                               ),
+                                       ),
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'buddypress',
+                                               ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 207, $response->get_status() );
+               $this->assertArrayHasKey( 'failed', $data );
+               $this->assertEquals( 'validation', $data['failed'] );
+               $this->assertCount( 2, $data['responses'] );
+               $this->assertNull( $data['responses'][0] );
+               $this->assertEquals( 400, $data['responses'][1]['status'] );
+               $this->assertFalse( get_option( 'test_project' ) );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_batch_v1_pre_validation_all_successful() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'methods'             => 'POST',
+                               'callback'            => static function ( $request ) {
+                                       return new WP_REST_Response( $request['project'] );
+                               },
+                               'permission_callback' => '__return_true',
+                               'allow_batch'         => array( 'v1' => true ),
+                               'args'                => array(
+                                       'project' => array(
+                                               'type' => 'string',
+                                               'enum' => array( 'gutenberg', 'WordPress' ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'validation' => 'require-all-validate',
+                               'requests'   => array(
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'gutenberg',
+                                               ),
+                                       ),
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'WordPress',
+                                               ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 207, $response->get_status() );
+               $this->assertArrayNotHasKey( 'failed', $data );
+               $this->assertCount( 2, $data['responses'] );
+               $this->assertEquals( 'gutenberg', $data['responses'][0]['body'] );
+               $this->assertEquals( 'WordPress', $data['responses'][1]['body'] );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_batch_v1() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test/(?P<id>[\d+])',
+                       array(
+                               'methods'             => array( 'POST', 'DELETE' ),
+                               'callback'            => function ( WP_REST_Request $request ) {
+                                       $this->assertEquals( 'DELETE', $request->get_method() );
+                                       $this->assertEquals( '/test-ns/v1/test/5', $request->get_route() );
+                                       $this->assertEquals( array( 'id' => '5' ), $request->get_url_params() );
+                                       $this->assertEquals( array( 'query' => 'param' ), $request->get_query_params() );
+                                       $this->assertEquals( array( 'project' => 'gutenberg' ), $request->get_body_params() );
+                                       $this->assertEquals( array( 'my_header' => array( 'my-value' ) ), $request->get_headers() );
+
+                                       return new WP_REST_Response( 'test' );
+                               },
+                               'permission_callback' => '__return_true',
+                               'allow_batch'         => array( 'v1' => true ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'requests' => array(
+                                       array(
+                                               'method'  => 'DELETE',
+                                               'path'    => '/test-ns/v1/test/5?query=param',
+                                               'headers' => array(
+                                                       'My-Header' => 'my-value',
+                                               ),
+                                               'body'    => array(
+                                                       'project' => 'gutenberg',
+                                               ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+
+               $this->assertEquals( 207, $response->get_status() );
+               $this->assertEquals( 'test', $response->get_data()['responses'][0]['body'] );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_batch_v1_partial_error() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'methods'             => 'POST',
+                               'callback'            => static function ( $request ) {
+                                       $project = $request['project'];
+                                       update_option( 'test_project', $project );
+
+                                       return new WP_REST_Response( $project );
+                               },
+                               'permission_callback' => '__return_true',
+                               'allow_batch'         => array( 'v1' => true ),
+                               'args'                => array(
+                                       'project' => array(
+                                               'type' => 'string',
+                                               'enum' => array( 'gutenberg', 'WordPress' ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'requests' => array(
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'gutenberg',
+                                               ),
+                                       ),
+                                       array(
+                                               'path' => '/test-ns/v1/test',
+                                               'body' => array(
+                                                       'project' => 'buddypress',
+                                               ),
+                                       ),
+                               ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 207, $response->get_status() );
+               $this->assertArrayNotHasKey( 'failed', $data );
+               $this->assertCount( 2, $data['responses'] );
+               $this->assertEquals( 'gutenberg', $data['responses'][0]['body'] );
+               $this->assertEquals( 400, $data['responses'][1]['status'] );
+               $this->assertEquals( 'gutenberg', get_option( 'test_project' ) );
+       }
+
+
+       /**
+        * @ticket 50244
+        */
+       public function test_batch_v1_max_requests() {
+               add_filter(
+                       'rest_get_max_batch_size',
+                       static function() {
+                               return 5;
+                       }
+               );
+
+               $GLOBALS['wp_rest_server'] = null;
+               add_filter( 'wp_rest_server_class', array( $this, 'filter_wp_rest_server_class' ) );
+               $GLOBALS['wp_rest_server'] = rest_get_server();
+
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test/(?P<id>[\d+])',
+                       array(
+                               'methods'             => array( 'POST', 'DELETE' ),
+                               'callback'            => function ( WP_REST_Request $request ) {
+                                       return new WP_REST_Response( 'test' );
+                               },
+                               'permission_callback' => '__return_true',
+                               'allow_batch'         => array( 'v1' => true ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'POST', '/batch/v1' );
+               $request->set_body_params(
+                       array(
+                               'requests' => array_fill( 0, 6, array( 'path' => '/test-ns/v1/test/5' ) ),
+                       )
+               );
+
+               $response = rest_get_server()->dispatch( $request );
+               $this->assertEquals( 400, $response->get_status() );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public function _validate_as_integer_123( $value, $request, $key ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! is_int( $value ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return new WP_Error( 'some-error', 'This is not valid!' );
</span></span></pre></div>
<a id="trunktestsqunitfixtureswpapigeneratedjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/wp-api-generated.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/wp-api-generated.js    2020-10-20 19:05:51 UTC (rev 49251)
+++ trunk/tests/qunit/fixtures/wp-api-generated.js      2020-10-20 19:08:48 UTC (rev 49252)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -41,6 +41,78 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 "self": "http://example.org/index.php?rest_route=/"
</span><span class="cx" style="display: block; padding: 0 10px">             }
</span><span class="cx" style="display: block; padding: 0 10px">         },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        "/batch/v1": {
+            "namespace": "",
+            "methods": [
+                "POST"
+            ],
+            "endpoints": [
+                {
+                    "methods": [
+                        "POST"
+                    ],
+                    "args": {
+                        "validation": {
+                            "required": false,
+                            "default": "normal",
+                            "enum": [
+                                "require-all-validate",
+                                "normal"
+                            ],
+                            "type": "string"
+                        },
+                        "requests": {
+                            "required": true,
+                            "type": "array",
+                            "items": {
+                                "type": "object",
+                                "properties": {
+                                    "method": {
+                                        "type": "string",
+                                        "enum": [
+                                            "POST",
+                                            "PUT",
+                                            "PATCH",
+                                            "DELETE"
+                                        ],
+                                        "default": "POST"
+                                    },
+                                    "path": {
+                                        "type": "string",
+                                        "required": true
+                                    },
+                                    "body": {
+                                        "type": "object",
+                                        "properties": [],
+                                        "additionalProperties": true
+                                    },
+                                    "headers": {
+                                        "type": "object",
+                                        "properties": [],
+                                        "additionalProperties": {
+                                            "type": [
+                                                "string",
+                                                "array"
+                                            ],
+                                            "items": {
+                                                "type": "string"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            ],
+            "_links": {
+                "self": [
+                    {
+                        "href": "http://example.org/index.php?rest_route=/batch/v1"
+                    }
+                ]
+            }
+        },
</ins><span class="cx" style="display: block; padding: 0 10px">         "/oembed/1.0": {
</span><span class="cx" style="display: block; padding: 0 10px">             "namespace": "oembed/1.0",
</span><span class="cx" style="display: block; padding: 0 10px">             "methods": [
</span></span></pre>
</div>
</div>

</body>
</html>