diff --git a/features/core-check-update.feature b/features/core-check-update.feature index 32ede965..63877dbf 100644 --- a/features/core-check-update.feature +++ b/features/core-check-update.feature @@ -70,3 +70,19 @@ Feature: Check for more recent versions """ --- """ + + Scenario: Check update shows warning when version check API fails + Given a WP install + And that HTTP requests to https://api.wordpress.org/core/version-check/1.7/ will respond with: + """ + HTTP/1.1 500 Internal Server Error + Content-Type: text/plain + + + """ + + When I try `wp core check-update --force-check` + Then STDERR should contain: + """ + Warning: Failed to check for updates + """ diff --git a/src/Core_Command.php b/src/Core_Command.php index 18d340f9..ae1c7397 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -31,6 +31,13 @@ */ class Core_Command extends WP_CLI_Command { + /** + * Stores HTTP API errors encountered during version check. + * + * @var \WP_Error|null + */ + private $version_check_error = null; + /** * Checks for WordPress updates via Version Check API. * @@ -92,6 +99,14 @@ public function check_update( $args, $assoc_args ) { [ 'version', 'update_type', 'package_url' ] ); $formatter->display_items( $updates ); + } elseif ( $this->version_check_error ) { + // If there was an HTTP error during version check, show a warning + WP_CLI::warning( + sprintf( + 'Failed to check for updates: %s', + $this->version_check_error->get_error_message() + ) + ); } else { WP_CLI::success( 'WordPress is at the latest version.' ); } @@ -1453,7 +1468,25 @@ private function get_download_url( $version, $locale = 'en_US', $file_type = 'zi */ private function get_updates( $assoc_args ) { $force_check = Utils\get_flag_value( $assoc_args, 'force-check' ); - wp_version_check( [], $force_check ); + + // Reset error tracking + $this->version_check_error = null; + + // Hook into pre_http_request with max priority to capture errors during version check + // This is necessary because tests and plugins may use pre_http_request to mock responses + add_filter( 'pre_http_request', [ $this, 'capture_version_check_error' ], PHP_INT_MAX, 3 ); + + // Also hook into http_api_debug to capture errors from real HTTP requests + // This fires when pre_http_request doesn't short-circuit the request + add_action( 'http_api_debug', [ $this, 'capture_version_check_error_from_response' ], 10, 5 ); + + try { + wp_version_check( [], $force_check ); + } finally { + // Ensure the hooks are always removed, even if wp_version_check() throws an exception + remove_filter( 'pre_http_request', [ $this, 'capture_version_check_error' ], PHP_INT_MAX ); + remove_action( 'http_api_debug', [ $this, 'capture_version_check_error_from_response' ], 10 ); + } /** * @var object{updates: array}|false $from_api @@ -1510,6 +1543,81 @@ private function get_updates( $assoc_args ) { return array_values( $updates ); } + /** + * Sets or clears the version check error property based on an HTTP response. + * + * @param mixed $response The HTTP response (WP_Error, array, or other). + */ + private function set_version_check_error( $response ) { + if ( is_wp_error( $response ) ) { + $this->version_check_error = $response; + } elseif ( is_array( $response ) && isset( $response['response']['code'] ) && $response['response']['code'] >= 400 ) { + // HTTP error status code (4xx or 5xx) - convert to WP_Error for consistency + $this->version_check_error = new \WP_Error( + 'http_request_failed', + sprintf( + 'HTTP request failed with status %d: %s', + $response['response']['code'], + isset( $response['response']['message'] ) ? $response['response']['message'] : 'Unknown error' + ) + ); + } else { + // Clear any previous error if we got a successful response. + $this->version_check_error = null; + } + } + + /** + * Handles the pre_http_request filter to capture HTTP errors during version check. + * + * This method signature matches the pre_http_request filter signature. + * Uses PHP_INT_MAX priority to run after test mocking and plugin modifications. + * + * @param false|array|WP_Error $response A preemptive return value of an HTTP request. + * @param array $args HTTP request arguments. + * @param string $url The request URL. + * @return false|array|WP_Error The response, unmodified. + */ + public function capture_version_check_error( $response, $args, $url ) { + if ( false === strpos( $url, 'api.wordpress.org/core/version-check' ) ) { + return $response; + } + + // A `false` response means the request is not being preempted. + // In this case, we don't want to change the error status, as the subsequent + // `http_api_debug` hook will handle the actual response. + if ( false !== $response ) { + $this->set_version_check_error( $response ); + } + + return $response; + } + + /** + * Handles the http_api_debug action to capture HTTP errors from real requests. + * + * This fires when pre_http_request doesn't short-circuit the request. + * Uses the http_api_debug action hook signature. + * + * @param array|WP_Error $response HTTP response or WP_Error object. + * @param string $context Context of the HTTP request. + * @param string $_class HTTP transport class name (unused). + * @param array $_args HTTP request arguments (unused). + * @param string $url URL being requested. + */ + public function capture_version_check_error_from_response( $response, $context, $_class, $_args, $url ) { + if ( false === strpos( $url, 'api.wordpress.org/core/version-check' ) ) { + return; + } + + // Only capture on response, not pre_response + if ( 'response' !== $context ) { + return; + } + + $this->set_version_check_error( $response ); + } + /** * Clean up extra files. *