WooCommerce Code Reference

UnexpectedErrorResponse.php

Source code

<?php
declare( strict_types = 1 );

namespace Automattic\WooCommerce\StoreApi\Utilities;

use Automattic\Jetpack\Constants;
use WP_Error;

/**
 * Builds safe Store API responses for unexpected engine failures.
 *
 * @internal
 */
final class UnexpectedErrorResponse {
	/**
	 * Maximum number of backtrace frames recorded in the log context.
	 */
	private const MAX_BACKTRACE_FRAMES = 10;

	/**
	 * Log an engine failure and create its Store API response.
	 *
	 * @since 11.3.0
	 *
	 * @param \Throwable  $error           Unexpected engine failure.
	 * @param string      $failure_context Context in which the failure occurred.
	 * @param string|null $public_message  Message shown to clients without debug access.
	 * @return WP_Error
	 */
	public static function create( \Throwable $error, string $failure_context, ?string $public_message = null ): WP_Error {
		$log_message = sprintf(
			'Store API request failed in %1$s: %2$s: %3$s in %4$s:%5$d',
			$failure_context,
			get_class( $error ),
			$error->getMessage(),
			$error->getFile(),
			$error->getLine()
		);

		// PHP reports subclasses of ParseError and CompileError as E_ERROR, so match exact classes.
		$error_types = array(
			\ParseError::class   => E_PARSE,
			\CompileError::class => E_COMPILE_ERROR,
		);

		try {
			wc_get_logger()->critical(
				$log_message,
				array(
					'source'         => 'store-api',
					'error'          => array(
						'type' => $error_types[ get_class( $error ) ] ?? E_ERROR,
						'file' => $error->getFile(),
						'line' => $error->getLine(),
					),
					// Same shape the fatal-error shutdown handler logs, so log handlers and readers see one trace format.
					'backtrace'      => array_slice( explode( "\n", $error->getTraceAsString() ), 0, self::MAX_BACKTRACE_FRAMES ),
					// These failures reached remote logging as fatals before the dispatchers caught them.
					'remote-logging' => true,
				)
			);
		} catch ( \Throwable $logging_error ) {
			// Logging must not prevent the safe response, but a failure that also broke logging should not vanish.
			error_log( $log_message . ' (logging failed: ' . $logging_error->getMessage() . ')' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		}

		$message = $public_message ?? __( 'Internal server error', 'woocommerce' );
		$data    = array( 'status' => 500 );

		if ( self::can_expose_details() ) {
			$message                 = $error->getMessage();
			$data['exception_class'] = get_class( $error );
		}

		return new WP_Error( 'woocommerce_rest_unknown_server_error', $message, $data );
	}

	/**
	 * Whether the current request may receive the failure message and class.
	 *
	 * @return bool
	 */
	private static function can_expose_details(): bool {
		/**
		 * Filters whether unexpected Store API failures include the error message and exception class in the response.
		 *
		 * Details are only ever sent to users who can manage WooCommerce; this filter cannot bypass that check. It defaults to WP_DEBUG so store staff can debug a production store without enabling debug mode site-wide.
		 *
		 * @since 11.3.0
		 *
		 * @param bool $expose_error_details Whether to include the error message and exception class. Defaults to WP_DEBUG.
		 *
		 * @return bool
		 */
		$expose_error_details = (bool) apply_filters( 'woocommerce_store_api_expose_error_details', Constants::is_true( 'WP_DEBUG' ) );

		return $expose_error_details && current_user_can( 'manage_woocommerce' );
	}
}