Automattic\WooCommerce\Api\Infrastructure

GraphQLControllerBase::process_requestprivateWC 1.0

Process the GraphQL request. Extracted so that handle_request() can wrap everything in a single try/catch that respects debug mode.

Method of the class: GraphQLControllerBase{}

No Hooks.

Returns

null. Nothing (null).

Usage

// private - for code of main (parent) class only
$result = $this->process_request( $request, $principal ): \WP_REST_Response;
$request(WP_REST_Request) (required)
The REST request.
$principal(object) (required)
The principal resolved by handle_request(); never null when this is reached.

GraphQLControllerBase::process_request() code WC 10.9.1

private function process_request( \WP_REST_Request $request, object $principal ): \WP_REST_Response {
	// 2. Parse request. GET query-string `variables` and `extensions`
	// arrive as JSON strings; decode_json_param() unifies them with the
	// already-decoded-array path from POST bodies and rejects malformed
	// or non-object payloads up front so they surface as HTTP 400
	// INVALID_ARGUMENT instead of as confusing resolver errors (null
	// decode) or HTTP 500 TypeErrors (scalar decode).
	$query          = $request->get_param( 'query' );
	$operation_name = $request->get_param( 'operationName' );
	$variables      = $this->decode_json_param( $request->get_param( 'variables' ), 'variables' );
	$extensions     = $this->decode_json_param( $request->get_param( 'extensions' ), 'extensions' );

	// 3. Resolve query (cache lookup / APQ / parse).
	$source = $this->query_cache->resolve( $query, $extensions );
	if ( is_array( $source ) ) {
		$default = $this->get_resolve_error_status( $source );
		return new \WP_REST_Response( $source, $this->pick_status( $default, $source, $request ) );
	}

	// 4. Reject mutations over GET (GraphQL over HTTP spec).
	if ( 'GET' === $request->get_method() && $this->document_has_mutation( $source, $operation_name ) ) {
		$method_not_allowed_output = array(
			'errors' => array(
				array(
					'message'    => 'Mutations are not allowed over GET requests. Use POST instead.',
					'extensions' => array( 'code' => 'METHOD_NOT_ALLOWED' ),
				),
			),
		);
		return new \WP_REST_Response(
			$method_not_allowed_output,
			$this->pick_status( 405, $method_not_allowed_output, $request )
		);
	}

	// 5. Load schema.
	$schema = $this->get_engine_schema();

	// 6. Build validation rules.
	// A single complexity-rule instance is kept so its computed score can
	// be surfaced in the debug extensions after execution.
	$complexity_rule    = new QueryComplexityRule( self::get_max_query_complexity() );
	$validation_rules   = array_values( DocumentValidator::allRules() );
	$validation_rules[] = new QueryDepthRule( self::get_max_query_depth() );
	$validation_rules[] = $complexity_rule;
	if ( ! $this->is_introspection_allowed( $principal, $request ) ) {
		$validation_rules[] = new DisableIntrospection( DisableIntrospection::ENABLED );
	}

	// 7. Execute. The context value is an ArrayObject (not a plain array)
	// so root resolvers can mutate it — specifically to thread the root
	// query's metadata into `$context['_query_metadata']` for downstream
	// field-level authorization gates. ArrayObject preserves the
	// `$context['key']` read syntax via ArrayAccess. The context carries
	// the resolved principal through to autogenerated resolvers, which
	// expose it as the `_principal` infrastructure parameter when commands
	// declare it on their authorize()/execute() methods. Request-derived
	// data that resolvers need is carried by the principal class itself —
	// populated by the PrincipalResolver, the only component wired to the
	// HTTP transport.
	$result = GraphQL::executeQuery(
		schema: $schema,
		source: $source,
		contextValue: new \ArrayObject(
			array(
				'principal' => $principal,
			)
		),
		variableValues: $variables,
		operationName: $operation_name,
		validationRules: $validation_rules,
	);

	// Install an error formatter that guarantees every error carries an
	// `extensions.code`. Our resolvers route everything through
	// Utils::execute_command / Utils::authorize_command, which already
	// translate domain exceptions (ApiException, InvalidArgumentException,
	// generic Throwable) into coded GraphQL errors at the throw site.
	// What reaches us uncoded here is webonyx-native validation and
	// execution output, so we infer from webonyx's ClientAware signal:
	// client-safe errors become BAD_USER_INPUT (400), the rest become
	// INTERNAL_ERROR (500).
	//
	// In debug mode the same formatter also walks the previous-exception
	// chain so wrapped errors (e.g. a \ValueError caught by a resolver and
	// re-thrown as INTERNAL_ERROR) stay visible to the developer instead
	// of being masked behind the generic "Internal server error" message.
	$debug_mode = $this->is_debug_mode( $principal, $request );
	$result->setErrorFormatter(
		function ( \Throwable $error ) use ( $debug_mode ): array {
			$formatted = \Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError::createFromException( $error );

			if ( ! isset( $formatted['extensions']['code'] ) ) {
				$client_safe                     = $error instanceof \Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware && $error->isClientSafe();
				$formatted['extensions']['code'] = $client_safe ? 'BAD_USER_INPUT' : 'INTERNAL_ERROR';
			}

			// SerializationError (thrown during schema-type coercion, e.g. when
			// a resolver returns an Int that doesn't fit 32 bits) extends
			// \Exception rather than webonyx's ClientAware Error, so it lands
			// in the INTERNAL_ERROR bucket above. Its message is actually
			// client-actionable ("value out of range — send smaller inputs"),
			// so promote it to BAD_USER_INPUT when it shows up anywhere in
			// the previous-exception chain.
			if ( 'BAD_USER_INPUT' !== ( $formatted['extensions']['code'] ?? null ) ) {
				$cursor = $error;
				while ( $cursor instanceof \Throwable ) {
					if ( $cursor instanceof \Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError ) {
						$formatted['extensions']['code'] = 'BAD_USER_INPUT';
						break;
					}
					$cursor = $cursor->getPrevious();
				}
			}

			if ( $debug_mode ) {
				$chain = $this->extract_previous_chain( $error );
				if ( ! empty( $chain ) ) {
					$formatted['extensions']['previous'] = $chain;
				}
			}

			return $formatted;
		}
	);

	$debug_flags = $this->get_debug_flags( $request, $principal );
	$output      = $result->toArray( $debug_flags );

	// 8. Debug-mode metrics: expose the computed complexity and depth so
	// clients tuning queries can see what the server scored the request at.
	if ( $this->is_debug_mode( $principal, $request ) ) {
		if ( ! isset( $output['extensions'] ) ) {
			$output['extensions'] = array();
		}
		if ( ! isset( $output['extensions']['debug'] ) ) {
			$output['extensions']['debug'] = array();
		}
		$output['extensions']['debug']['complexity'] = $complexity_rule->getQueryComplexity();
		$output['extensions']['debug']['depth']      = $this->compute_query_depth( $source, $operation_name );
	}

	// 9. Determine HTTP status code. GraphQL emits `data: { field: null }`
	// for nullable root fields even when the resolver errored, so gating
	// the status override on `data` being absent would leave nearly every
	// error response on HTTP 200. Always derive the status from the
	// errors array when one is present — clients that need "200 with
	// partial data" semantics can still read the `errors` array.
	$default = isset( $output['errors'] ) ? $this->get_error_status( $output['errors'] ) : 200;
	$status  = $this->pick_status( $default, $output, $request );

	return new \WP_REST_Response( $output, $status );
}