Batch (batch request)

In WordPress 5.6, the ability to perform a series of REST API requests in a single server request was introduced. In its simplest form, this is a useful performance optimization when a large number of write operations need to be executed. Additionally, basic concurrency management tools can optionally be used.

Registration

To use batch requests, routes must support this feature. It is enabled during registration. For example:

register_rest_route( 'my-ns/v1', 'my-route', [
	'methods'             => WP_REST_Server::CREATABLE,
	'callback'            => 'my_callback',
	'permission_callback' => 'my_permission_callback',
	'allow_batch'         => [ 'v1' => true ],
] );

If the REST API route has been implemented using best practices, declaring support should be sufficient for the route to be writable through the batch processing endpoint. In particular, here are some points to pay attention to:

  • Routes should use the WP_REST_Request object to retrieve all request data. In other words, it should not access the $_GET, $_POST, or $_SERVER variables to obtain parameters or headers.

  • Routes should return data. This can be a WP_REST_Response object, a WP_Error object, or any type of serializable JSON data. This means that the route should not directly output the response or use die(). For example, using wp_send_json() or wp_die().

  • Routes should be re-entrant. Be prepared for the same route to be called multiple times in a batch.

Executing a request

To send a batch request, make a POST request to /wp-json/batch/v1 with an array of the desired requests. For example, the simplest batch request looks like this.

{
  "requests": [
	{
	  "path": "/my-ns/v1/route"
	}
  ]
}

Request format

Each request is an object that can accept the following properties.

{
  "method": "PUT",
  "path": "/my-ns/v1/route/1?query=param",
  "headers": {
	"My-Header": "my-value",
	"Multi": [ "v1", "v2" ]
  },
  "body": {
	"project": "Gutenberg"
  }
}
  • method - the HTTP method that will be used for the request. If the method is omitted, POST is used.
  • path - the REST API route to call. Query parameters can be included. This property is required.
  • headers - an object containing header names and their values. If a header has multiple values, it can be passed as an array.
  • body - these are the parameters that need to be passed to the route. Filled in the type of POST parameters.

Discovering maximum requests

By default, the REST API accepts up to 25 requests in a single batch. However, this value is filterable, so it can be increased or decreased depending on server resources.

function my_prefix_rest_get_max_batch_size() {
	return 50;
}

add_filter( 'rest_get_max_batch_size', 'my_prefix_rest_get_max_batch_size' );

Therefore, clients are strongly encouraged to make a preliminary request to find out the limit. For example, an OPTIONS request to batch/v1 will return the following response.

{
  "namespace": "",
  "methods": ["POST"],
  "endpoints": [
	{
	  "methods": ["POST" ],
	  "args": {
		"validation": {
		  "type": "string",
		  "enum": ["require-all-validate", "normal" ],
		  "default": "normal",
		  "required": false
		},
		"requests": {
		  "type": "array",
		  "maxItems": 25,
		  "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"
				  }
				}
			  }
			}
		  },
		  "required": true
		}
	  }
	}
  ],
  "_links": {
	"self": [
	  {
		"href": "http://trunk.test/wp-json/batch/v1"
	  }
	]
  }
}

The limit is set in the property endpoints[0].args.requests.maxItems.

Response format

The batch processing endpoint returns a status code of 207 and responses to each request in the same order they were requested. For example:

{
  "responses": [
	{
	  "body": {
		"id": 1,
		"_links": {
		  "self": [
			{
			  "href": "http://trunk.test/wp-json/my-ns/v1/route/1"
			}
		  ]
		}
	  },
	  "status": 201,
	  "headers": {
		"Location": "http://trunk.test/wp-json/my-n1/v1/route/1",
		"Allow": "GET, POST"
	  }
	}
  ]
}

Internally, the REST API wraps each response before including it in the response array.

Validation modes

By default, each request is processed in isolation. This means that the batch response can contain several successful and several failed requests. Sometimes you want to process a batch only if all requests are valid. For example, in Gutenberg, we do not want to save some menu items; ideally, all should be saved or none.

To do this, the REST API allows passing the validation mode require-all-validate. When this parameter is set, the REST API will first check that each request is valid according to WP_REST_Request::has_valid_params() and WP_REST_Request::sanitize_params(). If any request fails validation, the entire batch will be rejected.

In this example, a batch of two requests is executed, and the second request failed validation. Since the order of responses does not differ from the order of requests, null is used to indicate that the request failed validation.

{
  "failed": "validation",
  "responses": [
	null,
	{
	  "body": {
		"code": "error_code",
		"message": "Invalid request data",
		"data": { "status": 400 }
	  },
	  "status": 400,
	  "headers": {}
	}
  ]
}

Note: Using require-all-validate does not guarantee that all requests will be successful. The route callback can still return an error.

Validation callback

The WP_REST_Request methods use the validate_callback and sanitize_callback callbacks specified for each parameter when registering the route. In most cases, this means validation based on a schema.

Any validation performed within the route, such as in the prepare_item_for_database method, will not cause the batch to be rejected. If this is a concern, it is recommended to move as much validation as possible into the validate_callback for each individual parameter. This can be done, for example, on top of existing schema-based validation.

'post' => array(
	'type' => 'integer',
	'minimum' => 1,
	'required' => true,
	'arg_options' => array(
		'validate_callback' => function ( $value, $request, $param ) {
			$valid = rest_validate_request_arg( $value, $request, $param );

			if ( is_wp_error( $valid ) ) {
				return $valid;
			}

			if ( ! get_post( $value ) || ! current_user_can( 'read_post', $value ) ) {
				return new WP_Error( 'invalid_post', __( 'This post does not exist.' ) );
			}

			return true;
		}
	)
)

Sometimes, full request context is required when performing validation. Usually, such validation was performed in prepare_item_for_database, but in WordPress 5.6, an alternative has been introduced. When registering a route, you can now specify a top-level validate_callback. It will receive the full WP_REST_Request object and can return an instance of WP_Error or false. The callback will not be executed if the parameter-level validation did not pass.

register_rest_route( 'my-ns/v1', 'route', array(
	'callback'            => '__return_empty_array',
	'permission_callback' => '__return_true',
	'validate_callback'   => function( $request ) {
		if ( $request['pass1'] !== $request['pass2'] ) {
			return new WP_Error(
				'passwords_must_match',
				__( 'Passwords must match.' ),
				array( 'status' => 400 )
			);
		}

		return true;
	}
) );

Note: Request validation occurs before access permissions are checked. Keep this in mind when considering whether to move logic into validate_callback.

Limitations

Currently, none of the built-in routes support batch processing. This will be added in a future release, likely starting immediately with WordPress 5.7.

GET requests are not supported. Developers are encouraged to use links and embedding or parallel requests.

--

See: https://make.wordpress.org/core/2020/11/20/rest-api-batch-framework-in-wordpress-5-6/