Skip to content

Instantly share code, notes, and snippets.

@jnthn
Last active August 28, 2018 06:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnthn/f41606739aad2b49126a3379a6a7961c to your computer and use it in GitHub Desktop.
Save jnthn/f41606739aad2b49126a3379a6a7961c to your computer and use it in GitHub Desktop.

Cro::HTTP::Router Middleware Tweaks for 0.8.0

The problem

Currently, before runs once we have matched a route and after runs only after the processing of a matched route. This is a useful semantic. It's perhaps not the best default semantic. Of note, it means that if you use before with a piece of authentication or authorization middleware, it will be too late, since the route matching may include matching on the session. Similarly, after can not be used to rewrite errors into appropriate behavior (for example, 401 into a redirect to an error page), because if a route was not matched due to failing authorization then we'd not run the after.

Thus, instead of writing:

my $app = route {
    before Some::Auth::Middleware.new(...);
    after { redirect '/login' if .status == 401 }
    get -> LoggedIn $user, 'foo' {
        ...
    }
}

One must write:

my $app = route {
    before Some::Auth::Middleware.new(...);
    after { redirect '/login' if .status == 401 }
    delegate <*> => route {
        get -> LoggedIn $user, 'foo' {
            ...
        }
    }
}

This is awkward to have to explain. It will become a FAQ, and the solution will be grudgingly copied and pasted. Heck, I designed the thing and I'm tired of working around it and explaining it, and this is when we only have a small userbase of patient early adopters. :-)

A separate problem is that before and after only apply to things textually after them. While the above was by-design behavior that I'm now reconsidering, this one is just an implementation accident.

Proposal

The current semantics of before and after will remain available, but take the names before-matched and after-matched - the past tense of "matched" hopefully providing a clue that they are applying to matched routes. The include semantics will be preserved too - that is, the before-matched and after-matched of an included route block will only apply to those rules inside of the block. The only difference will be that ordering no longer matters - that is, both before-matched and after-matched will apply to every route in the enclosing route block, not just those that are textually after it.

The names before and after will be recycled. They will quite literally mean "insert this into the pipeline before the route processing" and "insert this into the pipeline after the route processing". Whether a route in the route block matches or not will not be relevant. This means that the example given earlier for authorization will Just Work.

Unfortunately, we're left with a new design problem. Consider the following:

my $app = route {
    get -> 'foo', 'bar' { ... }
      include route {
          before Blah::Blah::Middleware.new;
          get -> 'foo' { ... }
      }
    }
}

Recall that include is much like a textual include, and - most significantly - the matching is done by a single matcher. Thus, before and after would have to mean adding to the pipeline before and after the route block that does the include. This is either:

  • Sensible enough, given that we describe include as "textualish". You could even use it to factor out a set of middleware applications.
  • Confusing. For one, we do make before-matched and after-matched only affect things in the included block, and that feels right. But there's no way we could do that for the new before and after semantics.

I'm worried people will write stuff like:

my $admin-stuff = route {
    before Some::Auth::Middleware;
    get -> Admin $user { ... }
    get -> Admin $user, 'users' { ... }
}
my $app = route {
    ...
    include admin => $admin-stuff;
}

And then get surprised that before and after apply to the entire outer route block. Probably they actually wanted to use delegate:

my $admin-stuff = route {
    before Some::Auth::Middleware;
    get -> Admin $user { ... }
    get -> Admin $user, 'users' { ... }
}
my $app = route {
    ...
    delegate <admin *> => $admin-stuff;
}

Probably the most conservative option is to produce an error if the target of an include has before or after, explaining why that's a problem, and suggesting either delegate or a switch to before-matched and after-matched.

@nige123
Copy link

nige123 commented Aug 28, 2018

jnthn - I kind of understand the inside/outside problem here. Also the tension between the simplicity of a textual include and then the temporal nature of request/pipeline processing. Aargh - the waterbed of complexity - messing up the smooth sheets again! ;-)

Just a couple of thoughts. Apache mod_perl was a beast - one redeeming feature however, was the clear Apache request cycle - that mod_perl integrated with. A middleware developer could write hooks for different phases of the cycle.

As in Perl 6 - maybe phasers could help with including something textually but operating at a different time? The middleware 'phasers' could fire in response to events in the request processing cycle. The potential downside here is the hidden complexity of stacked phase handlers and their interactions etc. Hope this helps in some way though ...

Thanks for Cro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment