Make WordPress Core


Ignore:
Timestamp:
10/20/2020 07:08:48 PM (4 years ago)
Author:
TimothyBlynJacobs
Message:

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 [48947] 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 #50244.
Props andraganescu, zieladam, TimothyBlynJacobs.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/rest-api/rest-server.php

    r48947 r49252  
    16181618    }
    16191619
     1620    /**
     1621     * @ticket       50244
     1622     * @dataProvider data_batch_v1_optin
     1623     */
     1624    public function test_batch_v1_optin( $allow_batch, $allowed ) {
     1625        $args = array(
     1626            'methods'             => 'POST',
     1627            'callback'            => static function () {
     1628                return new WP_REST_Response( 'data' );
     1629            },
     1630            'permission_callback' => '__return_true',
     1631        );
     1632
     1633        if ( null !== $allow_batch ) {
     1634            $args['allow_batch'] = $allow_batch;
     1635        }
     1636
     1637        register_rest_route(
     1638            'test-ns/v1',
     1639            '/test',
     1640            $args
     1641        );
     1642
     1643        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1644        $request->set_body_params(
     1645            array(
     1646                'requests' => array(
     1647                    array(
     1648                        'path' => '/test-ns/v1/test',
     1649                    ),
     1650                ),
     1651            )
     1652        );
     1653
     1654        $response = rest_do_request( $request );
     1655
     1656        $this->assertEquals( 207, $response->get_status() );
     1657
     1658        if ( $allowed ) {
     1659            $this->assertEquals( 'data', $response->get_data()['responses'][0]['body'] );
     1660        } else {
     1661            $this->assertEquals( 'rest_batch_not_allowed', $response->get_data()['responses'][0]['body']['code'] );
     1662        }
     1663    }
     1664
     1665    public function data_batch_v1_optin() {
     1666        return array(
     1667            'missing'             => array( null, false ),
     1668            'invalid type'        => array( true, false ),
     1669            'invalid type string' => array( 'v1', false ),
     1670            'wrong version'       => array( array( 'version1' => true ), false ),
     1671            'false version'       => array( array( 'v1' => false ), false ),
     1672            'valid'               => array( array( 'v1' => true ), true ),
     1673        );
     1674    }
     1675
     1676    /**
     1677     * @ticket 50244
     1678     */
     1679    public function test_batch_v1_pre_validation() {
     1680        register_rest_route(
     1681            'test-ns/v1',
     1682            '/test',
     1683            array(
     1684                'methods'             => 'POST',
     1685                'callback'            => static function ( $request ) {
     1686                    $project = $request['project'];
     1687                    update_option( 'test_project', $project );
     1688
     1689                    return new WP_REST_Response( $project );
     1690                },
     1691                'permission_callback' => '__return_true',
     1692                'allow_batch'         => array( 'v1' => true ),
     1693                'args'                => array(
     1694                    'project' => array(
     1695                        'type' => 'string',
     1696                        'enum' => array( 'gutenberg', 'WordPress' ),
     1697                    ),
     1698                ),
     1699            )
     1700        );
     1701
     1702        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1703        $request->set_body_params(
     1704            array(
     1705                'validation' => 'require-all-validate',
     1706                'requests'   => array(
     1707                    array(
     1708                        'path' => '/test-ns/v1/test',
     1709                        'body' => array(
     1710                            'project' => 'gutenberg',
     1711                        ),
     1712                    ),
     1713                    array(
     1714                        'path' => '/test-ns/v1/test',
     1715                        'body' => array(
     1716                            'project' => 'buddypress',
     1717                        ),
     1718                    ),
     1719                ),
     1720            )
     1721        );
     1722
     1723        $response = rest_get_server()->dispatch( $request );
     1724        $data     = $response->get_data();
     1725
     1726        $this->assertEquals( 207, $response->get_status() );
     1727        $this->assertArrayHasKey( 'failed', $data );
     1728        $this->assertEquals( 'validation', $data['failed'] );
     1729        $this->assertCount( 2, $data['responses'] );
     1730        $this->assertNull( $data['responses'][0] );
     1731        $this->assertEquals( 400, $data['responses'][1]['status'] );
     1732        $this->assertFalse( get_option( 'test_project' ) );
     1733    }
     1734
     1735    /**
     1736     * @ticket 50244
     1737     */
     1738    public function test_batch_v1_pre_validation_all_successful() {
     1739        register_rest_route(
     1740            'test-ns/v1',
     1741            '/test',
     1742            array(
     1743                'methods'             => 'POST',
     1744                'callback'            => static function ( $request ) {
     1745                    return new WP_REST_Response( $request['project'] );
     1746                },
     1747                'permission_callback' => '__return_true',
     1748                'allow_batch'         => array( 'v1' => true ),
     1749                'args'                => array(
     1750                    'project' => array(
     1751                        'type' => 'string',
     1752                        'enum' => array( 'gutenberg', 'WordPress' ),
     1753                    ),
     1754                ),
     1755            )
     1756        );
     1757
     1758        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1759        $request->set_body_params(
     1760            array(
     1761                'validation' => 'require-all-validate',
     1762                'requests'   => array(
     1763                    array(
     1764                        'path' => '/test-ns/v1/test',
     1765                        'body' => array(
     1766                            'project' => 'gutenberg',
     1767                        ),
     1768                    ),
     1769                    array(
     1770                        'path' => '/test-ns/v1/test',
     1771                        'body' => array(
     1772                            'project' => 'WordPress',
     1773                        ),
     1774                    ),
     1775                ),
     1776            )
     1777        );
     1778
     1779        $response = rest_get_server()->dispatch( $request );
     1780        $data     = $response->get_data();
     1781
     1782        $this->assertEquals( 207, $response->get_status() );
     1783        $this->assertArrayNotHasKey( 'failed', $data );
     1784        $this->assertCount( 2, $data['responses'] );
     1785        $this->assertEquals( 'gutenberg', $data['responses'][0]['body'] );
     1786        $this->assertEquals( 'WordPress', $data['responses'][1]['body'] );
     1787    }
     1788
     1789    /**
     1790     * @ticket 50244
     1791     */
     1792    public function test_batch_v1() {
     1793        register_rest_route(
     1794            'test-ns/v1',
     1795            '/test/(?P<id>[\d+])',
     1796            array(
     1797                'methods'             => array( 'POST', 'DELETE' ),
     1798                'callback'            => function ( WP_REST_Request $request ) {
     1799                    $this->assertEquals( 'DELETE', $request->get_method() );
     1800                    $this->assertEquals( '/test-ns/v1/test/5', $request->get_route() );
     1801                    $this->assertEquals( array( 'id' => '5' ), $request->get_url_params() );
     1802                    $this->assertEquals( array( 'query' => 'param' ), $request->get_query_params() );
     1803                    $this->assertEquals( array( 'project' => 'gutenberg' ), $request->get_body_params() );
     1804                    $this->assertEquals( array( 'my_header' => array( 'my-value' ) ), $request->get_headers() );
     1805
     1806                    return new WP_REST_Response( 'test' );
     1807                },
     1808                'permission_callback' => '__return_true',
     1809                'allow_batch'         => array( 'v1' => true ),
     1810            )
     1811        );
     1812
     1813        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1814        $request->set_body_params(
     1815            array(
     1816                'requests' => array(
     1817                    array(
     1818                        'method'  => 'DELETE',
     1819                        'path'    => '/test-ns/v1/test/5?query=param',
     1820                        'headers' => array(
     1821                            'My-Header' => 'my-value',
     1822                        ),
     1823                        'body'    => array(
     1824                            'project' => 'gutenberg',
     1825                        ),
     1826                    ),
     1827                ),
     1828            )
     1829        );
     1830
     1831        $response = rest_get_server()->dispatch( $request );
     1832
     1833        $this->assertEquals( 207, $response->get_status() );
     1834        $this->assertEquals( 'test', $response->get_data()['responses'][0]['body'] );
     1835    }
     1836
     1837    /**
     1838     * @ticket 50244
     1839     */
     1840    public function test_batch_v1_partial_error() {
     1841        register_rest_route(
     1842            'test-ns/v1',
     1843            '/test',
     1844            array(
     1845                'methods'             => 'POST',
     1846                'callback'            => static function ( $request ) {
     1847                    $project = $request['project'];
     1848                    update_option( 'test_project', $project );
     1849
     1850                    return new WP_REST_Response( $project );
     1851                },
     1852                'permission_callback' => '__return_true',
     1853                'allow_batch'         => array( 'v1' => true ),
     1854                'args'                => array(
     1855                    'project' => array(
     1856                        'type' => 'string',
     1857                        'enum' => array( 'gutenberg', 'WordPress' ),
     1858                    ),
     1859                ),
     1860            )
     1861        );
     1862
     1863        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1864        $request->set_body_params(
     1865            array(
     1866                'requests' => array(
     1867                    array(
     1868                        'path' => '/test-ns/v1/test',
     1869                        'body' => array(
     1870                            'project' => 'gutenberg',
     1871                        ),
     1872                    ),
     1873                    array(
     1874                        'path' => '/test-ns/v1/test',
     1875                        'body' => array(
     1876                            'project' => 'buddypress',
     1877                        ),
     1878                    ),
     1879                ),
     1880            )
     1881        );
     1882
     1883        $response = rest_get_server()->dispatch( $request );
     1884        $data     = $response->get_data();
     1885
     1886        $this->assertEquals( 207, $response->get_status() );
     1887        $this->assertArrayNotHasKey( 'failed', $data );
     1888        $this->assertCount( 2, $data['responses'] );
     1889        $this->assertEquals( 'gutenberg', $data['responses'][0]['body'] );
     1890        $this->assertEquals( 400, $data['responses'][1]['status'] );
     1891        $this->assertEquals( 'gutenberg', get_option( 'test_project' ) );
     1892    }
     1893
     1894
     1895    /**
     1896     * @ticket 50244
     1897     */
     1898    public function test_batch_v1_max_requests() {
     1899        add_filter(
     1900            'rest_get_max_batch_size',
     1901            static function() {
     1902                return 5;
     1903            }
     1904        );
     1905
     1906        $GLOBALS['wp_rest_server'] = null;
     1907        add_filter( 'wp_rest_server_class', array( $this, 'filter_wp_rest_server_class' ) );
     1908        $GLOBALS['wp_rest_server'] = rest_get_server();
     1909
     1910        register_rest_route(
     1911            'test-ns/v1',
     1912            '/test/(?P<id>[\d+])',
     1913            array(
     1914                'methods'             => array( 'POST', 'DELETE' ),
     1915                'callback'            => function ( WP_REST_Request $request ) {
     1916                    return new WP_REST_Response( 'test' );
     1917                },
     1918                'permission_callback' => '__return_true',
     1919                'allow_batch'         => array( 'v1' => true ),
     1920            )
     1921        );
     1922
     1923        $request = new WP_REST_Request( 'POST', '/batch/v1' );
     1924        $request->set_body_params(
     1925            array(
     1926                'requests' => array_fill( 0, 6, array( 'path' => '/test-ns/v1/test/5' ) ),
     1927            )
     1928        );
     1929
     1930        $response = rest_get_server()->dispatch( $request );
     1931        $this->assertEquals( 400, $response->get_status() );
     1932    }
     1933
    16201934    public function _validate_as_integer_123( $value, $request, $key ) {
    16211935        if ( ! is_int( $value ) ) {
Note: See TracChangeset for help on using the changeset viewer.