diff --git a/projects/packages/protect/changelog/add-protect-uptime-error-detail b/projects/packages/protect/changelog/add-protect-uptime-error-detail new file mode 100644 index 00000000000..5841dc915bb --- /dev/null +++ b/projects/packages/protect/changelog/add-protect-uptime-error-detail @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Include the upstream status and error in the uptime route's failure response, so a timeout, a 401, and a 500 can be told apart. diff --git a/projects/packages/protect/src/sections/class-monitor.php b/projects/packages/protect/src/sections/class-monitor.php index 55187d4ed62..0f515d49077 100644 --- a/projects/packages/protect/src/sections/class-monitor.php +++ b/projects/packages/protect/src/sections/class-monitor.php @@ -120,7 +120,7 @@ public function get_uptime() { if ( ! in_array( $history['code'], array( 401, 403 ), true ) ) { set_transient( self::UPTIME_TRANSIENT, 'failed', MINUTE_IN_SECONDS ); } - return $this->unavailable_error(); + return $this->unavailable_error( $history ); } $uptime = array( @@ -187,7 +187,8 @@ private function fetch_is_up() { * GET a wpcom/v2 site endpoint as the current user. * * @param string $path Path after `/sites/`. - * @return array The HTTP `code` (0 when the request failed) and the decoded `body`, null unless the code is 200. + * @return array The HTTP `code` (0 when the request failed), the decoded `body` (null unless the code is 200), + * and an `error` string describing a transport failure (null otherwise). */ private function request( $path ) { $response = Client::wpcom_json_api_request_as_user( @@ -197,19 +198,35 @@ private function request( $path ) { null, 'wpcom' ); - $code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); + if ( is_wp_error( $response ) ) { + return array( + 'code' => 0, + 'body' => null, + 'error' => $response->get_error_code() . ': ' . $response->get_error_message(), + ); + } + $code = (int) wp_remote_retrieve_response_code( $response ); return array( - 'code' => $code, - 'body' => 200 === $code ? json_decode( wp_remote_retrieve_body( $response ), true ) : null, + 'code' => $code, + 'body' => 200 === $code ? json_decode( wp_remote_retrieve_body( $response ), true ) : null, + 'error' => null, ); } /** - * The error returned when WordPress.com can't be reached. + * The error returned when WordPress.com can't be reached, carrying the upstream detail for debugging. * + * @param array $request The failing request from `request()`; empty for a cached failure with no detail to hand. * @return WP_Error */ - private function unavailable_error() { - return new WP_Error( 'uptime_unavailable', __( 'Uptime history is unavailable right now.', 'jetpack-protect-pkg' ), array( 'status' => 502 ) ); + private function unavailable_error( $request = array() ) { + $data = array( 'status' => 502 ); + if ( isset( $request['code'] ) ) { + $data['upstreamStatus'] = $request['code']; + } + if ( ! empty( $request['error'] ) ) { + $data['upstreamError'] = $request['error']; + } + return new WP_Error( 'uptime_unavailable', __( 'Uptime history is unavailable right now.', 'jetpack-protect-pkg' ), $data ); } } diff --git a/projects/packages/protect/tests/php/Monitor_Test.php b/projects/packages/protect/tests/php/Monitor_Test.php index c6a0c2cfe74..8d05183852c 100644 --- a/projects/packages/protect/tests/php/Monitor_Test.php +++ b/projects/packages/protect/tests/php/Monitor_Test.php @@ -217,12 +217,55 @@ public function test_get_uptime_reports_the_current_status_and_caches_an_unknown */ public static function provide_unusable_histories() { return array( - 'request failed' => array( new WP_Error( 'http_request_failed', 'Timed out' ), 1 ), - 'WordPress.com 500' => array( self::json_response( array(), 500 ), 1 ), - 'no days' => array( self::json_response( array() ), 1 ), - 'no valid days' => array( self::json_response( array( 'total' => array( 'status' => 'up' ) ) ), 1 ), - 'this user is refused' => array( self::json_response( array(), 403 ), 2 ), - 'this user is not known' => array( self::json_response( array(), 401 ), 2 ), + 'request failed' => array( + new WP_Error( 'http_request_failed', 'Timed out' ), + 1, + array( + 'status' => 502, + 'upstreamStatus' => 0, + 'upstreamError' => 'http_request_failed: Timed out', + ), + ), + 'WordPress.com 500' => array( + self::json_response( array(), 500 ), + 1, + array( + 'status' => 502, + 'upstreamStatus' => 500, + ), + ), + 'no days' => array( + self::json_response( array() ), + 1, + array( + 'status' => 502, + 'upstreamStatus' => 200, + ), + ), + 'no valid days' => array( + self::json_response( array( 'total' => array( 'status' => 'up' ) ) ), + 1, + array( + 'status' => 502, + 'upstreamStatus' => 200, + ), + ), + 'this user is refused' => array( + self::json_response( array(), 403 ), + 2, + array( + 'status' => 502, + 'upstreamStatus' => 403, + ), + ), + 'this user is not known' => array( + self::json_response( array(), 401 ), + 2, + array( + 'status' => 502, + 'upstreamStatus' => 401, + ), + ), ); } @@ -231,9 +274,10 @@ public static function provide_unusable_histories() { * * @param array|WP_Error $uptime_response WordPress.com's answer for the uptime history. * @param int $requests Requests made by two calls: 1 when the failure is cached for everyone. + * @param array $error_data The error data the 502 should carry, telling a timeout from a 401 from a 500. */ #[DataProvider( 'provide_unusable_histories' )] - public function test_get_uptime_is_a_502_when_the_history_is_unusable_and_caches_only_shared_failures( $uptime_response, $requests ) { + public function test_get_uptime_is_a_502_when_the_history_is_unusable_and_caches_only_shared_failures( $uptime_response, $requests, $error_data ) { $this->uptime_response = $uptime_response; $monitor = new Monitor(); @@ -242,7 +286,7 @@ public function test_get_uptime_is_a_502_when_the_history_is_unusable_and_caches $this->assertInstanceOf( WP_Error::class, $first ); $this->assertSame( 'uptime_unavailable', $first->get_error_code() ); - $this->assertSame( array( 'status' => 502 ), $first->get_error_data() ); + $this->assertSame( $error_data, $first->get_error_data() ); $this->assertInstanceOf( WP_Error::class, $second ); $this->assertCount( $requests, $this->requests ); }