Skip to content

Commit

Permalink
Implement per-route psgi middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrtj committed Oct 10, 2024
1 parent 4ca9f1e commit c7f76e1
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 65 deletions.
5 changes: 4 additions & 1 deletion Changes
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Revision history for Kelp
{{$NEXT}}
[New Interface]
- Added middleware_obj attribute to Kelp
- Added NEXT_APP method to Kelp
- Added psgi_middleware attribute to Kelp::Routes::Pattern

[Changes]
- Middleware building is now done using Kelp::Middleware
- Middleware building is now done using Kelp::Middleware, making it easier to customize
- Kelp now uses a dynamic PSGI execution chain, allowing adding PSGI middlewares at route level

2.18 - 2024-10-08
[New Interface]
Expand Down
112 changes: 74 additions & 38 deletions lib/Kelp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,59 @@ sub run
return $middleware->wrap($app);
}

sub _psgi_internal
{
my ($self, $match) = @_;
my $req = $self->req;
my $res = $self->res;

# Go over the entire route chain
for my $route (@$match) {

# Dispatch
$req->named($route->named);
$req->route_name($route->name);
my $data = $self->routes->dispatch($self, $route);

if ($route->bridge) {

# Is it a bridge? Bridges must return a true value to allow the
# rest of the routes to run. They may also have rendered
# something, in which case trust that and don't render 403 (but
# still end the execution chain)

if (!$data) {
$res->render_403 unless $res->rendered;
}
}
elsif (defined $data) {

# If the non-bridge route returned something, then analyze it and render it

# Handle delayed response if CODE
return $data if ref $data eq 'CODE';
$res->render($data) unless $res->rendered;
}

# Do not go any further if we got a render
last if $res->rendered;
}

# If nothing got rendered
if (!$res->rendered) {
$self->_run_hook(after_unrendered => ($match));
}

return $self->finalize;
}

sub NEXT_APP
{
return sub {
(shift @{$_[0]->{'kelp.execution_chain'}})->($_[0]);
};
}

sub psgi
{
my ($self, $env) = @_;
Expand All @@ -294,45 +347,12 @@ sub psgi
}

return try {
$env->{'kelp.execution_chain'} = [
(grep { defined } map { $_->psgi_middleware } @$match),
sub { $self->_psgi_internal($match) },
];

# Go over the entire route chain
for my $route (@$match) {

# Dispatch
$req->named($route->named);
$req->route_name($route->name);
my $data = $self->routes->dispatch($self, $route);

if ($route->bridge) {

# Is it a bridge? Bridges must return a true value to allow the
# rest of the routes to run. They may also have rendered
# something, in which case trust that and don't render 403 (but
# still end the execution chain)

if (!$data) {
$res->render_403 unless $res->rendered;
}
}
elsif (defined $data) {

# If the non-bridge route returned something, then analyze it and render it

# Handle delayed response if CODE
return $data if ref $data eq 'CODE';
$res->render($data) unless $res->rendered;
}

# Do not go any further if we got a render
last if $res->rendered;
}

# If nothing got rendered
if (!$res->rendered) {
my $c = $self->_run_hook(after_unrendered => ($match));
}

return $self->finalize;
return Kelp->NEXT_APP->($env);
}
catch {
my $exception = $_;
Expand Down Expand Up @@ -936,6 +956,22 @@ Example new JSON encoder type defined in config:
},
},
=head2 NEXT_APP
Helper method for giving Kelp back the control over PSGI application. It must
be used when declaring route-level middleware. It is context-independent and
can be called from C<Kelp> package.
use Plack::Builder;
builder {
enable 'SomeMiddleware';
Kelp->NEXT_APP;
}
Internally, it uses C<kelp.execution_chain> PSGI environment to dynamically
construct a wrapped PSGI app without too much overhead.
=head1 AUTHOR
Stefan Geneshky - minimal <at> cpan.org
Expand Down
58 changes: 34 additions & 24 deletions lib/Kelp/Manual.pod
Original file line number Diff line number Diff line change
Expand Up @@ -650,15 +650,15 @@ information and examples.

=head2 Adding middleware

Kelp, being Plack-centric, will let you easily add middleware. There are four
possible ways to add middleware to your application, and all three ways can be
used separately or together.
Kelp, being Plack-centric, will let you easily add middleware. There are many
ways to do this, but we recommend one of the methods described below.

=head3 Using the configuration

Adding middleware in your configuration is probably the easiest and best way for
you. This way you can load different middleware for each running mode, e.g.
C<Debug> in development only.
Adding middleware in your configuration is probably the easiest and best way
for you. This way you can load different middleware for each running mode, e.g.
C<Debug> in development only. All middleware loaded this way is global for your
application.

Add middleware names to the C<middleware> array in your configuration file and
the corresponding initializing arguments in the C<middleware_init> hash:
Expand All @@ -672,18 +672,40 @@ the corresponding initializing arguments in the C<middleware_init> hash:
}

The middleware will be added in the order you specify in the C<middleware>
array. See L<Kelp::Middleware/Advanced> for details.
array.

=head3 Middleware in routes

You can use Kelp's powerful router to find more middleware for your
application. This is done with C<psgi_middleware> field when adding a route:

use Plack::Builder;

$r->add('/checksummed' => {
to => 'get_content',
psgi_middleware => builder {
enable 'ContentMD5';
Kelp->NEXT_APP;
},
});

Now exact path C</checksummed> (and no other path) will have that PSGI
middleware assigned to it. You need to wrap special L<Kelp/NEXT_APP> for this
to work.

See L<Kelp::Routes/PLACK MIDDLEWARES> for details.

=head3 By subclassing L<Kelp::Middleware>

L<Kelp::Middleware> is a class which handles wrapping application in middleware
based on config. Subclassing it may be the most powerful way to add more
middleware if configuration is not enough.
middleware if default configuration is not enough.

# lib/MyApp.pm
attr middleware_obj => 'MyMiddleware';

# lib/MyMiddleware.pm
package MyMiddleware;
use Kelp::Base 'Kelp::Middleware';

sub wrap {
Expand All @@ -693,10 +715,13 @@ middleware if configuration is not enough.
return $app;
}

This lets you add middleware before or after config middleware (or disable config middleware completely).
This lets you add middleware before or after config middleware. You can also
come up with your own creative ways to use config for declaring middleware.

=head3 In C<app.psgi>

This is the same as adding middleware to vanilla Plack.

# app.psgi
use MyApp;
use Plack::Builder;
Expand All @@ -708,21 +733,6 @@ This lets you add middleware before or after config middleware (or disable confi
$app->run;
};

=head3 By overriding the L<Kelp/run> subroutine in C<lib/MyApp.pm>

Make sure you call C<SUPER> first, and then wrap new middleware around the
returned app.

# lib/MyApp.pm
sub run {
my $self = shift;
my $app = $self->SUPER::run(@_);
$app = Plack::Middleware::ContentLength->wrap($app);
return $app;
}

Note that any middleware defined in your config file will be added first.

=head2 Pluggable modules

=head3 How to load modules using the config
Expand Down
5 changes: 4 additions & 1 deletion lib/Kelp/Middleware.pm
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ Kelp::Middleware - Kelp app wrapper (PSGI middleware)
=head1 DESCRIPTION
This is a small helper object which wraps Kelp in PSGI middleware. It is loaded
This is a small helper class which wraps Kelp in PSGI middleware. It is loaded
and constructed by Kelp based on the value of L<Kelp/middleware_obj> (class
name).
This class only handles global middleware declared in configuration. Middleware
localized to routes cannot be adjusted by customizing this class.
=head1 ATTRIBUTES
=head2 app
Expand Down
42 changes: 42 additions & 0 deletions lib/Kelp/Routes.pm
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,48 @@ the paths set for the nested app will be wrong.
Note that a route cannot have C<psgi> and C<bridge> (or C<tree>) simultaneously.
=head1 PLACK MIDDLEWARES
If your route is not a Plack app and you want to reuse Plack middleware when
handling it, you may use C<psgi_middleware> and wrap L<Kelp/NEXT_APP>:
use Plack::Middleware::ContentMD5;
$r->add('/checksummed' => {
to => 'get_content',
psgi_middleware => Plack::Middleware::ContentMD5->wrap(Kelp->NEXT_APP),
});
You can also apply C<psgi_middleware> to bridges. Also, it is more readable to
use L<Plack::Builder> for this:
use Plack::Builder;
$r->add('/api' => {
to => sub { 1 }, # always pass through
bridge => 1,
psgi_middleware => builder {
enable 'Auth::Basic', authenticator => sub { ... };
Kelp->NEXT_APP;
},
});
Now everything under C</api> will go through this middleware. Note however that
C<psgi_middleware> is app-level middleware, not route-level. This means that
even if your bridge was to cut off traffic (return false value), all middleware
declared in routes will still have to run regardless, and it will run even
before the first route is executed. Don't think about it as I<"middleware for a
route">, but rather as I<"middleware for an app which is going to execute that
route">.
It is worth noting that using middleware in your routes will result in better
performance than global middleware. Having a ton of global middleware, even if
bound to a specific route, may result in quite a big overhead since it will
have to do a bunch of regular expression matches or string comparisons for
every route in your system. On the other hand, Kelp router is pretty optimized
and will only do the matching once, and only the matched routes will have to go
through this middleware.
=head1 ATTRIBUTES
=head2 base
Expand Down
6 changes: 6 additions & 0 deletions lib/Kelp/Routes/Pattern.pm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ attr named => sub { {} };
attr param => sub { [] };
attr to => undef;
attr dest => undef;
attr psgi_middleware => undef;

# helpers for matching different types of wildcards
sub __noslash
Expand Down Expand Up @@ -405,6 +406,11 @@ The loaded destination. An array reference with two values, a controller name
(or undef if not a controller) and the code reference to the method. It will be
automatically generated by the router based on the contents of L</to>.
=head2 psgi_middleware
Extra middleware for Kelp, for this route only. It must be a code reference,
and the middleware must wrap L<Kelp/NEXT_APP>.
=head1 METHODS
=head2 match
Expand Down
Loading

0 comments on commit c7f76e1

Please sign in to comment.