Implementing GraphQL Middleware

Middleware provides a convenient way to filter your GraphQL query or mutation. For example, it can be used to authenticate your user or check certain capabilities. Middleware refers to standalone classes which can be assigned to multiple resolvers. Middleware can be tested individually and reused throughout a plugin or the whole site.

For example, middleware can be used to check whether the user is authenticated (calling require_login) and interrupt the request if the user is not. If the user is authenticated, the middleware will allow the request to proceed further to the next middleware or to the resolver.

You can envision middleware as a series of layers around our resolvers which must be passed through. Each layer can examine the request, manipulate it, and even reject it. The same is true for the outgoing result, which has to pass through middleware (defined as after middleware) where the result can be examined or manipulated. 


Visual exmaple of middleware. The payload goes in and has to pass through a number of middleware before reaching the resolver. It then has to pass through middleware on its way out as well.

Defining middleware

Middleware classes are located in the webapi/middleware folder, and have to be in the matching namespace of the component.

They implement the middleware interface to ensure that they can be used by the default GraphQL resolver. 

<?php 

namespace my_component\webapi\middleware;

use Closure;
use coding_exception;
use core\entities\user;
use core\webapi\middleware;
use core\webapi\resolver\payload;
use core\webapi\resolver\result;

class verify_user implements middleware {

    /**
     * Handle the middleware before the resolver is called
     * 
     * @param payload $payload
     * @param Closure $next
     * @return result
     */
    public function handle(payload $payload, Closure $next): result {
        if (!$payload->user_id) {
            throw new moodle_exception('invaliduser');
        }

        $user = user::repository()->find($payload->user_id);
        if (!$user) {
            throw new moodle_exception('invaliduser');
        }

        return $next($payload);
    }
}

Before and after middleware

A middleware can run before or after the resolver got called. It depends on the middleware itself. For example, the following example would be called before the resolver is called:

namespace my_component\webapi\middleware;

use Closure;
use core\webapi\middleware;
use core\webapi\resolver\payload;
use core\webapi\resolver\result;

class before_middleware implements middleware {

    public function handle(payload $payload, Closure $next): result {
        // Do something before the resolver is called

        return $next($payload);
    }
}

Whereas this example would be called after the resolver got called. It can now read the result of the resolver and could react to it:

namespace my_component\webapi\middleware;

use Closure;
use core\webapi\middleware;
use core\webapi\resolver\payload;
use core\webapi\resolver\result;

class after_middleware implements middleware {

    public function handle(payload $payload, Closure $next): result {
        $result = $next($payload);

        // Do something after the resolver got called

        return $result;
    }
}

Middleware groups

Sometimes you want to group multiple middleware to make them easier to assign to resolvers. The group can be registered in a resolver, and any changes in the group would affect all resolvers in which this group is registered.

To define a group, implement the middleware_group interface:

namespace my_component\webapi\middleware;

use Closure;
use core\webapi\middleware_group;

class my_middleware_group implements middleware_group {

    public function get_middleware: array {
        return [
            middleware1::class,
            middleware2::class,
            middleware3::class,
        ];
    }
}

Middleware groups can be nested and will be resolved recursively.

Registering middleware for resolvers

Assigning middleware to resolvers

To assign one or more middleware classes to a resolver, implement the get_middleware method as shown below.

<?php

namespace my_component\webapi\resolver\query;

use core\webapi\execution_context;
use core\webapi\query_resolver;
use my_component\webapi\middleware\verify_user;

class my_graphql_query extends query_resolver {

    public static function resolve(array $args, execution_context $ec) {
		// Do something

        return $result;
    }

    /**
     * @inheritDoc
     */
    public static function get_middleware(): array {
        return [
            verify_user::class
        ];
    }

}

Configuring middleware

Sometimes a middleware might need extra parameters, which are different depending on the query in which it is used. In this case, you can implement a constructor which takes those arguments, and instead of returning a class name, return the instance:

    /**
     * @inheritDoc
     */
    public static function get_middleware(): array {
        return [
            new verify_user('user_id')
        ];
    }

Registering global middleware

It is also possible to assign middleware to be applied to all resolvers for a specific endpoint. This can be done by declaring the get_middleware() method in the endpoint_type class (defined in server/totara/webapi/classes/endpoint_type/):

    /**
     * @inheritDoc
     */
    public function get_middleware(): array {
        return array_merge(
            parent::get_middleware(),
            [
                client_rate_limit::class,
                global_rate_limit::class,
            ]
        );
    }

Alternatively, third-party plugins can modify the assigned middleware via an API hook. See Extending GraphQL APIs for more details.

Be aware that global middleware is applied to query, mutation and type resolvers, and therefore will be actioned multiple times during a single request. See below for more details on how middleware calls are applied in practice.

Understanding how middleware calls are applied to a request

Due to the asynchronous nature of the GraphQL library, the flow of the execution of a request can be a little hard to follow, particularly when there is also middleware involved, as this is applied via a recursive middleware chain as closures, on top of the already closure-based resolver code.

An example can help to understand the flow in more detail.

Imagine the following simple request:

query {
  totara_webapi_status {
    status
    timestamp
  }
}

If, in this case, we had the following middleware declared that would impact this query:

  • Middleware declared in the totara_webapi_status query resolver (local_query_middleware)
  • Middleware declared on the totara_webapi_status type resolver (local_type_middleware)
  • Global middleware declared on the endpoint we are using to execute the query (global_middleware)

The flow and order of the request would be as follows:

  • Execution of 'query'
    • Start of default resolver invoke() method for totara_webapi\webapi\resolver\query\status
    • End of default resolver invoke() method for totara_webapi\webapi\resolver\query\status
    • global_middleware handle() method before resolve()
    • local_query_middleware handle() method before resolve()
    • Start of middleware closure in default_resolver.php
    • status query resolver resolve() method
    • End of middleware closure in default_resolver.php
    • local_query_middleware handle() method after resolve()
    • global_middleware handle() method after resolve()
  • Execution of 'type' for 'status' field
    • Start of default resolver invoke() method for totara_webapi\webapi\resolver\type\status
    • End of default resolver invoke() method for totara_webapi\webapi\resolver\type\status
    • global_middleware handle() method before resolve()
    • local_type_middleware handle() method before resolve()
    • Start of middleware closure in default_resolver.php
    • status type resolver resolve() method
    • End of middleware closure in default_resolver.php
    • local_type_middleware handle() method after resolve()
    • global_middleware handle() method after resolve()
  • Execution of 'type' for 'timestamp' field
    • Start of default resolver invoke() method for totara_webapi\webapi\resolver\type\status
    • End of default resolver invoke() method for totara_webapi\webapi\resolver\type\status
    • global_middleware handle() method before resolve()
    • local_type_middleware handle() method before resolve()
    • Start of middleware closure in default_resolver.php
    • status type resolver resolve() method
    • End of middleware closure in default_resolver.php
    • local_type_middleware handle() method after resolve()
    • global_middleware handle() method after resolve()

Note how global_middleware is executed three times: once for the query and once for each field resolution by the type resolver.

If this is not desirable, you might want to only add your middleware to specific resolver types or exit early in your middleware based on information about the request.