Action composition in Mojolicious

Something about the routing in Mojolicious has been making things difficult, and Play Framework had the answer.

Full source code for these examples can be found on GitHub

Routing differences in Play Framework and Mojolicious

It wasn’t obvious at first, but the routing model in Mojolicious quickly becomes an unfathomable mess - and very difficult to debug.

Mojolicious

  • Routes are complicated, can be chained together, and can have bridges and conditions which control the way routes behave:
    • Conditions are synchronous, and can switch between sub-routes on a per-request basis
    • Bridges can be asynchronous, but always ultimately end at the same destination (e.g., you couldn’t have two identical routes running through different bridges, since Mojolicious couldn’t determine which to use or if a route even exists, which is why conditions are synchronous).
  • Controller actions are plain subs, called directly at run-time by the Mojolicious router
    • Actions manipulate the stash and call Mojolicious methods directly (e.g. render)
    • The return value from an action is of limited use

Play Framework:

  • Routes are simple - they’re written statically and have no concept of bridges or conditions.
    • One route can point to one action, limited flexibility at the routing layer
  • Actions are composed by nesting action functions, with outer functions calling inner functions
    • The action returns either a result, or a future result
    • Actions can choose to call other actions, or contain multiple nested actions inside one action

Action composition

At first, the Play Framework routing model seemed very inflexible. Routes are static, and there’s no concept of bridges, conditions or any other way to intercept a request.

With Mojolicious, bridges and chained routes make its easy to abstract the ‘how I get there’ logic away from the ‘once I’ve got here’ logic - and the Play Framework way of life didn’t seem to offer the same flexibility.

Play Framework uses action composition, which means nesting actions inside other actions, and it felt a bit too restrictive.

But action composition quickly proved to be the more powerful of the two. And definitely the easiest to understand, especially when tracing requests through a complicated web application.

Why the Mojolicious model starts well, but ends badly

The Mojolicious model is extremely powerful for a simple application - we can define route stubs or bridges, attach more routes to those, intercept requests in bridges and return 404 or 500 errors. Which is great - but then we end in a messy refactoring nightmare:

  • The code used in routing (e.g. authenticated bridges) needs to be shared between applications
  • We move the code into a shared library, and get it to create named bridges
  • We update our applications to use the named bridge from the shared library

All good so far.

Now we want to use a hook to intercept some part of the request - adding headers for example:

  • We write a plugin which registers a hook
  • The hook adds custom headers to a response
  • We include the plugin in our application, and all responses get new headers

Still good. But once we’ve done this a few times, we end up with all routing and request manipulation being done by shared libraries. We have limited code visibility, and all we get to show for our efforts are:

  • A few ->plugin lines, adding ‘unknown’ functionality to our routing

$app->plugin('My::Magic::Routing::Plugin');
$app->plugin('Another::Magic::Routing::Plugin');

  • An dangerous action sub, with no idea how the request gets there

sub this_should_be_authenticated_and_authorised {
  $self->delete_all_data;
  $self->make_everyone_admin;
  $self->enter_maintenance_mode;
  $self->render(text => 'hope I really was authenticated...');
}

And now we’re potentially screwed.

Mojolicious routing has become well hidden technical debt (or a serious defect/PR disaster) waiting to bite.

Why the Play Framework model is better

Although routing becomes far more static in Play Framework, we can still refactor our routing code into shared libraries.

But there’s one important difference.

With Play Framework action composition, we maintain full code visibility at the controller:

def index = Authenticated { request =>
  Authorised(request, request.user) {
    Ok(request.user.get)
  } otherwise {
    Unauthorized
  }
}

Instead of tracing through multiple plugins to find out what happens to a request, it’s all there in front of us. We know the request is Authenticated, and is then Authorised. If we have a bug, its easy to follow a request to see what happens.

We can also choose how much code visibility we get - for example, Authenticated takes care of what happens if user authentication fails, but Authorised leaves it to the developer to decide how to handle an authorisation failure.

So what can we do about it

This is Perl - there’s always a way!

Let’s define the syntax first:

# a plain action
Action 'welcome' => sub {
  shift->render(text => '');
};

# an asynchronous action (without a render_later call)
Action 'login' => Async {
  my $self = shift;
  Mojo::IOLoop->timer(0.25 => sub {
    $self->render(text => '');
  });
};

# an authenticated action
Action 'private1' => Authenticated {
  shift->render(text => '');
};

# nested actions, with parameters
Action 'private2' => Async { Authenticated {
  WithPermission 'blog.post' => sub {
    shift->render(text => '');
  }, sub {
    shift->render(text => 'not authorised');
  }
}};

Implementing it in Mojolicious

To start with, we need a way to define an action. This is essentially the same as the default sub{}, but lets us capture its contents.

The basic Action action

We need to be able to pass in a sub, and return a sub (though, for the top-level Action, we’ll need to monkey patch it so the Mojolicious router can find it).

Since we need to be able to chain these actions together, we also need to recursively call the inner action. We’ll also need to do that for any other actions we define, so lets make it generic:

sub go {
  my ($controller, $inner) = @_;
  my $i = $inner;
  while($i) {
    my $res = $i->($controller) if ref($i) eq 'CODE';
    $i = ref($res) eq 'CODE' ? $res : undef;
  }
  return undef;
}

sub Action($$) {
  my ($action, $inner) = @_;
  monkey_patch caller, $action => sub { go(shift, $inner) }
}

This is enough to let us define a new action like this:

Action 'myaction' => sub {
    shift->render(text => '');
};

A nested action - Async

Adding another action type is just as easy.

Since Async is our first ‘nested’ action, we’ll implement that:

sub Async(&) {
  my $inner = shift;
  return sub {
    my $controller = shift;
    $controller->render_later;
    go($controller, $inner);
  }
}

Now we can define an Async action, without needing to call render_later:

Action 'login' => Async {
  my $self = shift;
  Mojo::IOLoop->timer(0.25 => sub {
    $self->session(auth => 1);
    $self->render(text => '');
  });
};

An action with parameters

We’ll skip over the Authenticated action for now - its almost identical to Async, with the exception of needing to perform a session lookup to decide whether to continue the action chain.

Instead, we’ll implement WithPermission - an action with run-time parameters.

We need to be able to pass in some custom parameters, and a sub, and have it return a sub which, like the others, invokes the inner sub when its called:

sub WithPermission($&;&) {
  my ($permission, $inner, $error) = @_;
  return sub {
    my $controller = shift;
    if($permission eq 'blog.delete') {
      go($controller, $inner);
    } else {
      $error ? go($controller, $error) : $controller->render_exception('Unauthorised');
    }
    return undef;
  }
}

Which lets us define an action like this:

Action 'private1' => Authenticated { WithPermission 'blog.post' => sub {
  shift->render(text => '');
}, sub {
  shift->render(text => 'need permission blog.post');
}};

If the user is authenticated and has the permission ‘blog.post’, the first inner action gets executed, otherwise the second is called instead. However, since our WithPermission action only accepts ‘blog.delete’, this will always fail.

The WithPermission action also implements a default failure action, so we could skip the second inner action completely.

Summary

The simple implementation above gives us the flexibility to move our bridge and condition code away from the routing and into the controller layer, improving code visibility for developers.

We can get many of the benefits that Play Framework offers, but there’s still one big difference.

In Play Framework, we could invoke an action and inspect its result, and still choose to not return that content to the user. This gives us the flexibility to invoke multiple actions, and decide later which to use, for example:

def index = Authenticated { request =>
  val res1 = Foo(request);
  val res2 = Bar(request);
  if((res1.body.json \ 'some_key').isDefined)
    Authorised(res1, request.user) {
  else
    Unauthorized
}

In Mojolicious, the client might end up with a mixture of both responses, or just whichever gets called first. Because Mojolicious doesn’t use the return value, as soon as an action calls ->render it will immediately return a response to the client.

It should be possible - by creating a new stash and fake response objects, and possibly patching ->render - to trick Mojolicious into supporting a future/promise based interface.