<!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>[48947] trunk: REST API: Refactor `WP_REST_Server::dispatch()` to make internal logic reusable.</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/48947">48947</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/48947","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-09-05 21:50:31 +0000 (Sat, 05 Sep 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: Refactor `WP_REST_Server::dispatch()` to make internal logic reusable.

<a href="https://core.trac.wordpress.org/ticket/50244">#50244</a> aims to introduce batch processing in the REST API. An important feature is the ability to enforce that all requests have valid data before executing the route callbacks in "pre-validate" mode.

This necessitates splitting `WP_REST_Server::dispatch()` into two methods so the batch controller can determine the request handler to perform pre-validation and then respond to the requests.

The two new methods, `match_request_to_handler` and `respond_to_request`, have a public visibility, but are marked as `@access private`. This is to allow for iteration on the batch controller to happen in the Gutenberg repository. Developers should not rely upon these methods, their visibility may change in the future.

See <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>
</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-09-05 18:10:47 UTC (rev 48946)
+++ trunk/src/wp-includes/rest-api/class-wp-rest-server.php     2020-09-05 21:50:31 UTC (rev 48947)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -911,6 +911,48 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return $result;
</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">+                $error   = null;
+               $matched = $this->match_request_to_handler( $request );
+
+               if ( is_wp_error( $matched ) ) {
+                       return $this->error_to_response( $matched );
+               }
+
+               list( $route, $handler ) = $matched;
+
+               if ( ! is_callable( $handler['callback'] ) ) {
+                       $error = new WP_Error(
+                               'rest_invalid_handler',
+                               __( 'The handler for the route is invalid' ),
+                               array( 'status' => 500 )
+                       );
+               }
+
+               if ( ! is_wp_error( $error ) ) {
+                       $check_required = $request->has_valid_params();
+                       if ( is_wp_error( $check_required ) ) {
+                               $error = $check_required;
+                       } else {
+                               $check_sanitized = $request->sanitize_params();
+                               if ( is_wp_error( $check_sanitized ) ) {
+                                       $error = $check_sanitized;
+                               }
+                       }
+               }
+
+               return $this->respond_to_request( $request, $route, $handler, $error );
+       }
+
+       /**
+        * Matches a request object to it's handler.
+        *
+        * @access private
+        * @since 5.6.0
+        *
+        * @param WP_REST_Request $request The request object.
+        * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
+        */
+       public 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">@@ -957,142 +999,136 @@
</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">                                if ( ! is_callable( $callback ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $response = new WP_Error(
-                                               'rest_invalid_handler',
-                                               __( 'The handler for the route is invalid' ),
-                                               array( 'status' => 500 )
-                                       );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 return array( $route, $handler );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! is_wp_error( $response ) ) {
-                                       // Remove the redundant preg_match argument.
-                                       unset( $args[0] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $request->set_url_params( $args );
+                               $request->set_attributes( $handler );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $request->set_url_params( $args );
-                                       $request->set_attributes( $handler );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $defaults = array();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $defaults = array();
-
-                                       foreach ( $handler['args'] as $arg => $options ) {
-                                               if ( isset( $options['default'] ) ) {
-                                                       $defaults[ $arg ] = $options['default'];
-                                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         foreach ( $handler['args'] as $arg => $options ) {
+                                       if ( isset( $options['default'] ) ) {
+                                               $defaults[ $arg ] = $options['default'];
</ins><span class="cx" style="display: block; padding: 0 10px">                                         }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-                                       $request->set_default_params( $defaults );
-
-                                       $check_required = $request->has_valid_params();
-                                       if ( is_wp_error( $check_required ) ) {
-                                               $response = $check_required;
-                                       } else {
-                                               $check_sanitized = $request->sanitize_params();
-                                               if ( is_wp_error( $check_sanitized ) ) {
-                                                       $response = $check_sanitized;
-                                               }
-                                       }
</del><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /**
-                                * Filters the response before executing any REST API callbacks.
-                                *
-                                * Allows plugins to perform additional validation after a
-                                * request is initialized and matched to a registered route,
-                                * but before it is executed.
-                                *
-                                * Note that this filter will not be called for requests that
-                                * fail to authenticate or match to a registered route.
-                                *
-                                * @since 4.7.0
-                                *
-                                * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
-                                * @param array                                            $handler  Route handler used for the request.
-                                * @param WP_REST_Request                                  $request  Request used to generate the response.
-                                */
-                               $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $request->set_default_params( $defaults );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! is_wp_error( $response ) ) {
-                                       // Check permission specified on the route.
-                                       if ( ! empty( $handler['permission_callback'] ) ) {
-                                               $permission = call_user_func( $handler['permission_callback'], $request );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         return array( $route, $handler );
+                       }
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                if ( is_wp_error( $permission ) ) {
-                                                       $response = $permission;
-                                               } elseif ( false === $permission || null === $permission ) {
-                                                       $response = new WP_Error(
-                                                               'rest_forbidden',
-                                                               __( 'Sorry, you are not allowed to do that.' ),
-                                                               array( 'status' => rest_authorization_required_code() )
-                                                       );
-                                               }
-                                       }
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return new WP_Error(
+                       'rest_no_route',
+                       __( 'No route was found matching the URL and request method' ),
+                       array( 'status' => 404 )
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( ! is_wp_error( $response ) ) {
-                                       /**
-                                        * Filters the REST dispatch request result.
-                                        *
-                                        * Allow plugins to override dispatching the request.
-                                        *
-                                        * @since 4.4.0
-                                        * @since 4.5.0 Added `$route` and `$handler` parameters.
-                                        *
-                                        * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
-                                        * @param WP_REST_Request $request         Request used to generate the response.
-                                        * @param string          $route           Route matched for the request.
-                                        * @param array           $handler         Route handler used for the request.
-                                        */
-                                       $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * Dispatches the request to the callback handler.
+        *
+        * @access private
+        * @since 5.6.0
+        *
+        * @param WP_REST_Request $request  The request object.
+        * @param array           $handler  The matched route handler.
+        * @param string          $route    The matched route regex.
+        * @param WP_Error|null   $response The current error object if any.
+        *
+        * @return WP_REST_Response
+        */
+       public function respond_to_request( $request, $route, $handler, $response ) {
+               /**
+                * Filters the response before executing any REST API callbacks.
+                *
+                * Allows plugins to perform additional validation after a
+                * request is initialized and matched to a registered route,
+                * but before it is executed.
+                *
+                * Note that this filter will not be called for requests that
+                * fail to authenticate or match to a registered route.
+                *
+                * @since 4.7.0
+                *
+                * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
+                * @param array                                            $handler  Route handler used for the request.
+                * @param WP_REST_Request                                  $request  Request used to generate the response.
+                */
+               $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        // Allow plugins to halt the request via this filter.
-                                       if ( null !== $dispatch_result ) {
-                                               $response = $dispatch_result;
-                                       } else {
-                                               $response = call_user_func( $callback, $request );
-                                       }
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Check permission specified on the route.
+               if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
+                       $permission = call_user_func( $handler['permission_callback'], $request );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /**
-                                * Filters the response immediately after executing any REST API
-                                * callbacks.
-                                *
-                                * Allows plugins to perform any needed cleanup, for example,
-                                * to undo changes made during the {@see 'rest_request_before_callbacks'}
-                                * filter.
-                                *
-                                * Note that this filter will not be called for requests that
-                                * fail to authenticate or match to a registered route.
-                                *
-                                * Note that an endpoint's `permission_callback` can still be
-                                * called after this filter - see `rest_send_allow_header()`.
-                                *
-                                * @since 4.7.0
-                                *
-                                * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
-                                * @param array                                            $handler  Route handler used for the request.
-                                * @param WP_REST_Request                                  $request  Request used to generate the response.
-                                */
-                               $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( is_wp_error( $permission ) ) {
+                               $response = $permission;
+                       } elseif ( false === $permission || null === $permission ) {
+                               $response = new WP_Error(
+                                       'rest_forbidden',
+                                       __( 'Sorry, you are not allowed to do that.' ),
+                                       array( 'status' => rest_authorization_required_code() )
+                               );
+                       }
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( is_wp_error( $response ) ) {
-                                       $response = $this->error_to_response( $response );
-                               } else {
-                                       $response = rest_ensure_response( $response );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! is_wp_error( $response ) ) {
+                       /**
+                        * Filters the REST dispatch request result.
+                        *
+                        * Allow plugins to override dispatching the request.
+                        *
+                        * @since 4.4.0
+                        * @since 4.5.0 Added `$route` and `$handler` parameters.
+                        *
+                        * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
+                        * @param WP_REST_Request $request         Request used to generate the response.
+                        * @param string          $route           Route matched for the request.
+                        * @param array           $handler         Route handler used for the request.
+                        */
+                       $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $response->set_matched_route( $route );
-                               $response->set_matched_handler( $handler );
-
-                               return $response;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Allow plugins to halt the request via this filter.
+                       if ( null !== $dispatch_result ) {
+                               $response = $dispatch_result;
+                       } else {
+                               $response = call_user_func( $handler['callback'], $request );
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->error_to_response(
-                       new WP_Error(
-                               'rest_no_route',
-                               __( 'No route was found matching the URL and request method' ),
-                               array( 'status' => 404 )
-                       )
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /**
+                * Filters the response immediately after executing any REST API
+                * callbacks.
+                *
+                * Allows plugins to perform any needed cleanup, for example,
+                * to undo changes made during the {@see 'rest_request_before_callbacks'}
+                * filter.
+                *
+                * Note that this filter will not be called for requests that
+                * fail to authenticate or match to a registered route.
+                *
+                * Note that an endpoint's `permission_callback` can still be
+                * called after this filter - see `rest_send_allow_header()`.
+                *
+                * @since 4.7.0
+                *
+                * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
+                * @param array                                            $handler  Route handler used for the request.
+                * @param WP_REST_Request                                  $request  Request used to generate the response.
+                */
+               $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
+
+               if ( is_wp_error( $response ) ) {
+                       $response = $this->error_to_response( $response );
+               } else {
+                       $response = rest_ensure_response( $response );
+               }
+
+               $response->set_matched_route( $route );
+               $response->set_matched_handler( $handler );
+
+               return $response;
</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></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-09-05 18:10:47 UTC (rev 48946)
+++ trunk/tests/phpunit/tests/rest-api/rest-server.php  2020-09-05 21:50:31 UTC (rev 48947)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1513,6 +1513,110 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( 204, $response->get_status(), '/test-ns/v1/test' );
</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
+        */
+       public function test_no_route() {
+               $mock_hook = new MockAction();
+               add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
+
+               $response = rest_do_request( '/test-ns/v1/test' );
+               $this->assertErrorResponse( 'rest_no_route', $response, 404 );
+
+               // Verify that the no route error was not filtered.
+               $this->assertCount( 0, $mock_hook->get_events() );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_invalid_handler() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'callback'            => 'invalid_callback',
+                               'permission_callback' => '__return_true',
+                       )
+               );
+
+               $mock_hook = new MockAction();
+               add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
+
+               $response = rest_do_request( '/test-ns/v1/test' );
+               $this->assertErrorResponse( 'rest_invalid_handler', $response, 500 );
+
+               // Verify that the invalid handler error was filtered.
+               $events = $mock_hook->get_events();
+               $this->assertCount( 1, $events );
+               $this->assertWPError( $events[0]['args'][0] );
+               $this->assertEquals( 'rest_invalid_handler', $events[0]['args'][0]->get_error_code() );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_callbacks_are_not_executed_if_request_validation_fails() {
+               $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
+               $callback->expects( self::never() )->method( '__invoke' );
+               $permission_callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
+               $permission_callback->expects( self::never() )->method( '__invoke' );
+
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'callback'            => $callback,
+                               'permission_callback' => $permission_callback,
+                               'args'                => array(
+                                       'test' => array(
+                                               'validate_callback' => '__return_false',
+                                       ),
+                               ),
+                       )
+               );
+
+               $request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
+               $request->set_query_params( array( 'test' => 'world' ) );
+               $response = rest_do_request( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+       }
+
+       /**
+        * @ticket 50244
+        */
+       public function test_filters_are_executed_if_request_validation_fails() {
+               register_rest_route(
+                       'test-ns/v1',
+                       '/test',
+                       array(
+                               'callback'            => '__return_empty_array',
+                               'permission_callback' => '__return_true',
+                               'args'                => array(
+                                       'test' => array(
+                                               'validate_callback' => '__return_false',
+                                       ),
+                               ),
+                       )
+               );
+
+               $mock_hook = new MockAction();
+               add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
+
+               $request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
+               $request->set_query_params( array( 'test' => 'world' ) );
+               $response = rest_do_request( $request );
+
+               $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+               // Verify that the invalid param error was filtered.
+               $events = $mock_hook->get_events();
+               $this->assertCount( 1, $events );
+               $this->assertWPError( $events[0]['args'][0] );
+               $this->assertEquals( 'rest_invalid_param', $events[0]['args'][0]->get_error_code() );
+       }
+
</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>
</div>

</body>
</html>