Chains of synchronous handlers

The problem

JavaScript framework Express has influenced RESTinio a lot. For example, an Express-like router has been added in early versions of RESTinio and become one of the most used RESTinio’s features.

But some parts of the original Express framework weren’t adopted for RESTinio. Most notable Express’s middleware. There is no such feature in RESTinio.

The absence of Express’s middleware made the handling of some cases more difficult than we wanted.

For example, consider a HTTP-entry point to a C++ application that have to do several things for each incoming request:

  • check the presence and values of several custom HTTP-fields;
  • authenticate a user if an incoming request is going to /admin and /stats entry-points;
  • log information about the user if an incoming request is going to /admin entry-point;
  • perform actual processing of an incoming request.

The simple and obvious solution in RESTinio prior to v.0.6.13 could look like this:

restinio::request_handling_status_t
request_handler(restinio::request_handle_t req)
{
   if(ensure_valid_fields(req))
   {
      return req->create_response(restinio::status_bad_request())
         ...
         .body(...)
         .done();
   }

   optional<authentificated_user> user_info;
   if("/admin" == req->header().path() || "/stats" == req->header().path())
   {
      const auto auth_result = try_authentificate(req);
      if(!auth_result)
      {
         return req->create_response(restinio::status_bad_request())
            ...
            .body(...)
            .done();
      }
   }

   if("/admin" == req->header().path())
   {
      ...
   }

   return do_actual_processing(std::move(req), user_info, permissions);
}

This approach isn’t very flexible. What if we need to add another intermediate step? What if we need to turn some step off/on in dependency of settings in the config file?

This approach also isn’t a composable one. Just imagine that you write several applications (services) with HTTP-entries, and all of them have the similar authentication step. How easy the reusing of try_authentificate would be?

Middleware from Express framework provides a more simple, flexible and reusable way of doing such tasks. Since v.0.6.13 RESTinio provides something similar in the form of a chain of synchronous handlers.

The solution in common words

The solution RESTinio provides since v.0.6.13 is the use of a special composite request-handler named “a chain”. That chain contains a list of user-provided request-handlers and calls them sequentially from the first to the last. If the current handler in the list returns request_handling_status_t::not_handled the next handler in the chain is called.

The invocation of handlers stops when the current returns request_handling_status_t::accepted or request_handling_status_t::rejected. Or when the end of the chain reached.

If the end of chain is reached but the request wasn’t accepted or rejected then the chain returns request_handling_status_t::not_handled.

So the solution with a chain can look like that (please note that this is just a sketch, not a real code):

restinio::request_handling_status_t fields_checker(
   const restinio::request_handle_t & req)
{
   ... // Checks. Generation of negative response if necessary.

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t authentificate_user(
   const restinio::request_handle_t & req)
{
   if("/admin" == req->header().path() || "/stats" == req->header().path())
   {
      ... // Authentification. Generation of negative response if necessary.
   }

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t log_admin_access(
   const restinio::request_handle_t & req)
{
   if("/admin" == req->header().path())
   {
      ... // Logging of the access to admin panel.
   }

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t actual_processing(
   const restinio::request_handle_t & req)
{
   ... // The actual processing of a request.
}

// Declaration of new request-handler type in server's traits.
struct my_traits : public restinio::default_traits_t
{
   using request_handler_t = some_handler_chain_type;
};

// Run the server and specify chained handlers.
restinio::run(restinio::on_thread_pool(16)
   .port(...)
   .address(...)
   .request_handler(
      // All those parameters will be passed to the constructor
      // of some_handler_chain_type.
      fields_checker,
      authentificate_user,
      log_admin_access,
      actual_processing)
   ...
   );

How to pass a data from one step to another?

In the code above we can see the problem in the chained handlers approach: the distribution of a data generated on some step to further steps. Thus an information about a user from authentication step should be available on authorization step. But how to make data exchange between handlers in the chain?

The extra-data-factory feature should be used in such a case (see Extra-data in request object).

For example:

struct my_extra_data_factory
{
   // Type of data for exchange between handlers in the chain.
   struct data_t
   {
      std::optional<authentificated_user> user_info_;
   };

   void make_within(restinio::extra_data_buffer_t<data_t> buf)
   {
      new(buf.get()) data_t{};
   }
};

// For simplicity.
using request_handle = restinio::generic_request_handle_t<
   my_extra_data_factory::data_t>;

// NOTE: the use of our `request_handle` type instead of
// `restinio::request_handle_t`.
restinio::request_handling_status_t fields_checker(
   const request_handle & req)
{
   ... // Checks. Generation of negative response if necessary.

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t authentificate_user(
   const request_handle & req)
{
   if("/admin" == req->header().path() || "/stats" == req->header().path())
   {
      ... // Authentification. Generation of negative response if necessary.
      req->extra_data().user_info_ = user_info; // Update request-related data.
   }

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t log_admin_access(
   const restinio::request_handle_t & req)
{
   if("/admin" == req->header().path())
   {
      // Log with the use of user info created on the previous step.
      log_admin_access_attempt(req->extra_data().user_info_.value());
   }

   return restinio::request_not_handled(); // Can try the next step.
}

restinio::request_handling_status_t actual_processing(
   const request_handle & req)
{
   ... // The actual processing of a request.
}

// We should specify my_extra_data_factory in traits.
struct my_traits : public restinio::default_traits_t
{
   using extra_data_factory_t = my_user_data_factory;
   // Request handler should know about our per-request data.
   using request_handler_t = some_handler_chain_type<extra_data_factory_t>;
};

// Run the server and specify chained handlers.
restinio::run(restinio::on_thread_pool(16)
   .port(...)
   .address(...)
   .request_handler(
      fields_checker,
      authentificate_user,
      log_admin_access,
      actual_processing)
   ...
   );

Why a chain of synchonous handlers?

RESTinio supports synchronous and asynchronous processing of requests, it’s a responsibility of a user to select the appropriate one. But in the case of a chain of handlers only synchronous processing has sense. It’s because RESTinio assumes that if one handler returns accepted or rejected then the actual processing of a request is completed or delegated to some other work context.

If the request is already completed (e.g. create_response() and then done() is called) then should not be any other actions with the request.

If the request’s processing is delegated to some other context, then we have no right to do something with the request in the current context. Because it can lead to data-races. So if the current handler in the chain returns a value different from not_handled, then RESTinio stops processing the chain.

It doesn’t mean that all request processing should be done synchronously. One of your handlers can delegate the actual processing or postpone it (by using timers, for example). But it means that all previous handlers should do their work only synchronously and all subsequent handlers will be ignored. For example:

restinio::request_handling_status_t first_handler(
   const restinio::request_handle_t & req)
{
   ... // All processing should be done synchronously.
   return restinio::request_not_handled();
}

restinio::request_handling_status_t second_handler(
   const restinio::request_handle_t & req)
{
   ... // All processing should be done synchronously.
   return restinio::request_not_handled();
}

restinio::request_handling_status_t third_handler(
   const restinio::request_handle_t & req)
{
   // Delegating the processing to another worker thread.
   workers_pool_.post(req);
   // We can't return request_not_handled here.
   // Inform RESTinio that request is accepted.
   return restinio::request_accepted();
}

restinio::request_handling_status_t fourth_handler(
   const restinio::request_handle_t & req)
{
   // NOTE! This handler won't be called!
}

If you need to chain several asynchronous processing actions (e.g. a handler delegates processing to one worker thread, then that worker delegates processing to another worker thread, and so on), then you have to implement that chaining by yourself. RESTinio’s haven’t such functionality now.

Ready-to-use implementations of chains

RESTinio contains several ready-to-use implementations of chains of handlers. This section briefly describes them.

Please note that the corresponding header files with the implementation of those chains are not included in restinio/core.hpp. Those headers should be included in your code manually.

fixed_size_chain

#include <restinio/sync_chain/fixed_size.hpp>

The template class restinio::sync_chain::fixed_size_chain_t is an implementation of a chain of handlers of fixed size and this size is known at the compile time:

template<
   std::size_t Size,
   typename Extra_Data_Factory = restinio::no_extra_data_factory_t >
class fixed_size_chain_t;

Usage example for the case when no extra-data is incorporated into a request object:

struct my_traits : public restinio::default_traits_t {
   using request_handler_t = restinio::sync_chain::fixed_size_chain_t<3>;
};

restinio::run(restinio::on_this_thread<my_traits>()
   .port(...)
   .address(...)
   // Exactly 3 handlers should be passed to request_handler().
   .request_handler(
      first_handler,
      second_handler,
      third_handler)
   ...);

Note that an instance of fixed_size_chain_t<N> accepts exact N handlers in the constructor and that list of handlers can’t be changed latter.

Usage example for the case when some extra-data is incorporated into a request object:

struct per_request_data {...};

struct my_traits : public restinio::default_traits_t {
   using extra_data_factory_t = restinio::simple_extra_data_factory_t<
         per_request_data>;

   using request_handler_t = restinio::sync_chain::fixed_size_chain_t<
         3,
         extra_data_factory_t>;
};

restinio::run(restinio::on_this_thread<my_traits>()
   .port(...)
   .address(...)
   // Exactly 3 handlers should be passed to request_handler().
   .request_handler(
      first_handler,
      second_handler,
      third_handler)
   ...);

growable_size_chain

#include <restinio/sync_chain/growable_size.hpp>

The template class restinio::sync_chain::growable_size_chain_t is an implementation of a chain of handlers of unknown size:

template<
   typename Extra_Data_Factory = restinio::no_extra_data_factory_t >
class growable_size_chain_t
{
public:
   class builder_t;
   ...
};

An instance of restinio::sync_chain::growable_size_chain_t can’t be created directly. It should be created and filled up by using special helper growable_size_chain_t::builder_t:

struct my_traits : public restinio::default_traits_t {
   using request_handler_t = restinio::sync_chain::growable_size_chain_t<>;
   ...
};

// Make a builder instance.
my_traits::request_handler_t::builder_t chain_builder;

// Pass all necessary handlers to the chain.
if(config.check_headers())
   chain_builder.add(header_checker);
chain_builder.add(authentificate_user);
if(config.log_admin_actions())
   chain_builder.add(log_admin_access);

// Run the server.
restinio::run(restinio::on_this_thread<my_traits>()
   .port(...)
   .address(...)
   .request_handler(
      // Release the instance of growable_size_chain and pass it
      // to the server.
      chain_builder.release())
   ...);

Please note that growable_size_chain_t::builder_t is like std::unique_ptr: it’s a Movable class, but not Copyable.

Also growable_size_chain_t::builder_t is intended for building of just one instance of growable_size_chain_t. After the call to builder_t::release the builder becomes empty and it must not be used without the full reinitialization.

Once a list of handlers is filled up in a builder and release() method is called for the builder, the list can’t be changed anymore.

Usage example for the case when some extra-data is incorporated into a request object:

struct per_request_data {...};

struct my_traits : public restinio::default_traits_t {
   using extra_data_factory_t = restinio::simple_extra_data_factory_t<
         per_request_data>;

   using request_handler_t = restinio::sync_chain::growable_size_chain_t<
         extra_data_factory_t>;
};

my_traits::request_handler_t::builder_t builder;

... // Fill the builder.

restinio::run(restinio::on_this_thread<my_traits>()
   .port(...)
   .address(...)
   .request_handler(builder.release())
   ...);