Merge branch 'master' into gsoc_breadboard
André Walker [Tue, 13 Dec 2011 19:57:39 +0000 (17:57 -0200)]
25 files changed:
Changes
Makefile.PL
lib/Catalyst.pm
lib/Catalyst/Action.pm
lib/Catalyst/DispatchType/Chained.pm
lib/Catalyst/Engine.pm
lib/Catalyst/PSGI.pod
lib/Catalyst/Runtime.pm
lib/Catalyst/Script/CGI.pm
lib/Catalyst/Script/Create.pm
lib/Catalyst/Script/FastCGI.pm
lib/Catalyst/Script/Server.pm
lib/Catalyst/Script/Test.pm
lib/Catalyst/ScriptRole.pm
lib/Catalyst/ScriptRunner.pm
t/aggregate/live_component_controller_action_chained.t
t/aggregate/unit_core_script_cgi.t
t/aggregate/unit_core_script_fastcgi.t
t/aggregate/unit_core_script_run_options.t [new file with mode: 0644]
t/aggregate/unit_core_script_server.t
t/author/spelling.t
t/lib/ScriptTestApp.pm [new file with mode: 0644]
t/lib/ScriptTestApp/Controller/Root.pm [new file with mode: 0644]
t/lib/TestApp/Action/TestMatchCaptures.pm [new file with mode: 0644]
t/lib/TestApp/Controller/Action/Chained.pm

diff --git a/Changes b/Changes
index 100f2da..b17fa35 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,5 +1,49 @@
 # This file documents the revision history for Perl extension Catalyst.
 
+5.90007 - 2011-11-22 20:35:00
+
+  New features:
+   - Implement a match_captures hook which, if it exists on an action,
+     is called with the $ctx and \@captures and is expected to return
+     true to continue the chain matching and false to stop matching.
+     This can be used to implement action classes or roles which match
+     conditionally (for example only matching captures which are integers).
+
+  Bug fixes:
+   - Lighttpd script name fix is only applied for lighttpd versions
+     < 1.4.23. This should fix non-root installs of lighttpd in versions
+     over that.
+   - Prepare_action is now inside a try {} block, so that requests containing
+     bad unicode can be appropriately trapped by
+     Catalyst::Plugin::Unicode::Encoding
+
+5.90006 - 2011-10-25 09:18:00
+
+  New features:
+   - A new 'run_options' class data method has been added to Catalyst.pm
+     This is used to store all the options passed by scripts, allowing
+     application authors to add custom options to their scripts then
+     get them passed through to the application.
+
+  Doumentation:
+   - Clarify that if you manually write your own .psgi file, then optional
+     proxy support (via the using_frontend_proxy config value) will not be
+     enabled unless you explicitly apply the default middlewares from
+     Catalyst, or you apply the middleware manually.
+
+  Bug fixes:
+   - Fix issue due to perl internals bugs in 5.8 and 5.10 (not present in
+     other perl versions) require can pass the context inappropriately,
+     meaning that some methods of loading classes can fail due to void
+     context being passed throuh to make_immutable, causing it to not return
+     a value.
+     This bug caused loading Catalyst::Script::XXX to fail and is fixed
+     both by bumping the Class::Load dependency, and also adding an explicit
+     '1;' to the end of the classes, avoiding the context issue.
+
+   - Fix using_frontend_proxy support in mod_perl by using the psgi wrapped
+     in default middleware in mod_perl context, rather than the raw psgi.
+
 5.90005 - 2011-10-22 13:35:00
 
   New features:
index 0d2b118..a600ea5 100644 (file)
@@ -19,7 +19,7 @@ requires 'namespace::autoclean' => '0.09';
 requires 'namespace::clean' => '0.13';
 requires 'B::Hooks::EndOfScope' => '0.08';
 requires 'MooseX::Emulate::Class::Accessor::Fast' => '0.00903';
-requires 'Class::Load' => '0.08';
+requires 'Class::Load' => '0.12';
 requires 'Class::MOP' => '0.95';
 requires 'Data::OptList';
 requires 'Moose' => '1.03';
index 61e2803..596e6d1 100644 (file)
@@ -74,7 +74,7 @@ our $GO        = Catalyst::Exception::Go->new;
 __PACKAGE__->mk_classdata($_)
   for qw/container arguments dispatcher engine log dispatcher_class
   engine_loader context_class request_class response_class stats_class
-  setup_finished _psgi_app loading_psgi_file/;
+  setup_finished _psgi_app loading_psgi_file run_options/;
 
 __PACKAGE__->dispatcher_class('Catalyst::Dispatcher');
 __PACKAGE__->request_class('Catalyst::Request');
@@ -83,7 +83,7 @@ __PACKAGE__->stats_class('Catalyst::Stats');
 
 # Remember to update this in Catalyst::Runtime as well!
 
-our $VERSION = '5.90005';
+our $VERSION = '5.90007';
 
 sub import {
     my ( $class, @arguments ) = @_;
@@ -1224,11 +1224,12 @@ to interpolate all the parameters in the URI.
 
 =item @args?
 
-Optional list of extra arguments - can be supplied in the C<< \@captures_and_args? >>
-array ref, or here - whichever is easier for your code..
+Optional list of extra arguments - can be supplied in the
+C<< \@captures_and_args? >> array ref, or here - whichever is easier for your
+code.
 
-If your action may have a zero, a fixed or a variable number of args (e.g. C<< Args(1) >>
-for a fixed number or C<< Args() >> for a variable number)..
+Your action can have zero, a fixed or a variable number of args (e.g.
+C<< Args(1) >> for a fixed number or C<< Args() >> for a variable number)..
 
 =item \%query_values?
 
@@ -1403,6 +1404,16 @@ sub welcome_message {
 EOF
 }
 
+=head2 run_options
+
+Contains a hash of options passed from the application script, including
+the original ARGV the script received, the processed values from that
+ARGV and any extra arguments to the script which were not processed.
+
+This can be used to add custom options to your application's scripts
+and setup your application differently depending on the values of these
+options.
+
 =head1 INTERNAL METHODS
 
 These methods are not meant to be used by end users.
@@ -1862,6 +1873,7 @@ sub prepare {
                 $c->prepare_body;
             }
         }
+        $c->prepare_action;
     }
     # VERY ugly and probably shouldn't rely on ->finalize actually working
     catch {
@@ -1869,19 +1881,19 @@ sub prepare {
         $c->response->status(400);
         $c->response->content_type('text/plain');
         $c->response->body('Bad Request');
+        # Note we call finalize and then die here, which escapes
+        # finalize being called in the enclosing block..
+        # It in fact couldn't be called, as we don't return $c..
+        # This is a mess - but I'm unsure you can fix this without
+        # breaking compat for people doing crazy things (we should set
+        # the 400 and just return the ctx here IMO, letting finalize get called
+        # above...
         $c->finalize;
         die $_;
     };
 
-    my $method  = $c->req->method  || '';
-    my $path    = $c->req->path;
-    $path       = '/' unless length $path;
-    my $address = $c->req->address || '';
-
     $c->log_request;
 
-    $c->prepare_action;
-
     return $c;
 }
 
@@ -2419,7 +2431,7 @@ sub setup_engine {
 
         $meta->add_method(handler => sub {
             my $r = shift;
-            my $psgi_app = $class->psgi_app;
+            my $psgi_app = $class->_finalized_psgi_app;
             $apache->call_app($r, $psgi_app);
         });
 
@@ -2510,7 +2522,16 @@ sub apply_default_middlewares {
 
     # If we're running under Lighttpd, swap PATH_INFO and SCRIPT_NAME
     # http://lists.scsys.co.uk/pipermail/catalyst/2006-June/008361.html
-    $psgi_app = Plack::Middleware::LighttpdScriptNameFix->wrap($psgi_app);
+    $psgi_app = Plack::Middleware::Conditional->wrap(
+        $psgi_app,
+        builder   => sub { Plack::Middleware::LighttpdScriptNameFix->wrap($_[0]) },
+        condition => sub {
+            my ($env) = @_;
+            return unless $env->{SERVER_SOFTWARE} && $env->{SERVER_SOFTWARE} =~ m!lighttpd[-/]1\.(\d+\.\d+)!;
+            return unless $1 < 4.23;
+            1;
+        },
+    );
 
     # we're applying this unconditionally as the middleware itself already makes
     # sure it doesn't fuck things up if it's not running under one of the right
@@ -2910,7 +2931,26 @@ headers.
 
 If you do not wish to use the proxy support at all, you may set:
 
-    MyApp->config(ignore_frontend_proxy => 1);
+    MyApp->config(ignore_frontend_proxy => 0);
+
+=head2 Note about psgi files
+
+Note that if you supply your own .psgi file, calling
+C<< MyApp->psgi_app(@_); >>, then B<this will not happen automatically>.
+
+You either need to apply L<Plack::Middleware::ReverseProxy> yourself
+in your psgi, for example:
+
+    builder {
+        enable "Plack::Middleware::ReverseProxy";
+        MyApp->psgi_app
+    };
+
+This will unconditionally add the ReverseProxy support, or you need to call
+C<< $app = MyApp->apply_default_middlewares($app) >> (to conditionally
+apply the support depending upon your config).
+
+See L<Catalyst::PSGI> for more information.
 
 =head1 THREAD SAFETY
 
index 09f81a8..af60527 100644 (file)
@@ -172,6 +172,17 @@ Returns the number of captures this action expects for L<Chained|Catalyst::Dispa
 
 Provided by Moose.
 
+=head1 OPTIONAL METHODS
+
+=head2 match_captures
+
+Can be implemented by action class and action role authors. If the method
+exists, then it will be called with the request context and an array reference
+of the captures for this action.
+
+Returning true from this method causes the chain match to continue, returning
+makes the chain not match (and alternate, less preferred chains will be attempted).
+
 =head1 AUTHORS
 
 Catalyst Contributors, see Catalyst.pm
index 9c32258..ede2b69 100644 (file)
@@ -211,6 +211,9 @@ sub recurse_match {
                 # strip CaptureArgs into list
                 push(@captures, splice(@parts, 0, $capture_attr->[0]));
 
+                # check if the action may fit, depending on a given test by the app
+                if ($action->can('match_captures')) { next TRY_ACTION unless $action->match_captures($c, \@captures) }
+
                 # try the remaining parts against children of this action
                 my ($actions, $captures, $action_parts, $n_pathparts) = $self->recurse_match(
                                              $c, '/'.$action->reverse, \@parts
@@ -404,6 +407,7 @@ sub expand_action {
 }
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 USAGE
 
@@ -673,6 +677,13 @@ The C<forward>ing to other actions does just what you would expect. But if
 you C<detach> out of a chain, the rest of the chain will not get called
 after the C<detach>.
 
+=head2 match_captures
+
+A method which can optionally be implemented by actions to
+stop chain matching.
+
+See L<Catalyst::Action> for further details.
+
 =head1 AUTHORS
 
 Catalyst Contributors, see Catalyst.pm
index 1e56bdc..8f88cef 100644 (file)
@@ -678,7 +678,7 @@ sub prepare_read {
 
 =head2 $self->prepare_request(@arguments)
 
-Populate the context object from the request object.
+Sets up the PSGI environment in the Engine.
 
 =cut
 
@@ -833,13 +833,13 @@ sub run {
         # instead the $app->handle method is called per request.
         $app->log->warn("Not supplied a Plack engine, falling back to engine auto-loader (are your scripts ancient?)")
     }
+    $app->run_options($options);
     $server->run($psgi, $options);
 }
 
 =head2 build_psgi_app ($app, @args)
 
-Builds and returns a PSGI application closure, wrapping it in the reverse proxy
-middleware if the using_frontend_proxy config setting is set.
+Builds and returns a PSGI application closure. (Raw, not wrapped in middleware)
 
 =cut
 
index bdd2ae9..a05e002 100644 (file)
@@ -74,8 +74,6 @@ in:
 
 =item L<Plack::Middleware::IIS6ScriptNameFix>
 
-=item nginx - local to Catalyst
-
 =back
 
 If you override the default by providing your own C<< .psgi >> file,
@@ -88,6 +86,15 @@ An apply_default_middlewares method is supplied to wrap your application
 in the default middlewares if you want this behaviour and you are providing
 your own .psgi file.
 
+This means that the auto-generated (no .psgi file) code looks something
+like this:
+
+    use strict;
+    use warnings;
+    use TestApp;
+
+    my $app = TestApp->apply_default_middlewares(TestApp->psgi_app(@_));
+
 =head1 SEE ALSO
 
 L<Catalyst::Upgrading>, L<Plack>, L<PSGI::FAQ>, L<PSGI>.
index b2c4837..34e3edc 100644 (file)
@@ -7,7 +7,7 @@ BEGIN { require 5.008004; }
 
 # Remember to update this in Catalyst as well!
 
-our $VERSION = '5.90005';
+our $VERSION = '5.90007';
 
 =head1 NAME
 
index 3a9f449..e05fffb 100644 (file)
@@ -7,6 +7,7 @@ sub _plack_engine_name { 'CGI' }
 with 'Catalyst::ScriptRole';
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 NAME
 
index 721c192..529b8c6 100644 (file)
@@ -51,6 +51,7 @@ sub run {
 }
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 NAME
 
index c3d2360..ba6ab9b 100644 (file)
@@ -105,11 +105,12 @@ sub _plack_loader_args {
     return %args;
 }
 
-sub _application_args {
-    my ($self) = shift;
+around _application_args => sub {
+    my ($orig, $self) = @_;
     return (
         $self->listen,
         {
+            %{ $self->$orig },
             nproc       => $self->nproc,
             pidfile     => $self->pidfile,
             manager     => $self->manager,
@@ -118,9 +119,10 @@ sub _application_args {
             proc_title  => $self->proc_title,
         }
     );
-}
+};
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 NAME
 
index ed8aef0..4e87191 100644 (file)
@@ -260,13 +260,13 @@ sub _plack_loader_args {
     );
 }
 
-sub _application_args {
-    my ($self) = shift;
+around _application_args => sub {
+    my ($orig, $self) = @_;
     return (
         $self->port,
         $self->host,
         {
-           argv => $self->ARGV,
+           %{ $self->$orig },
            map { $_ => $self->$_ } qw/
                 fork
                 keepalive
@@ -274,13 +274,14 @@ sub _application_args {
                 pidfile
                 keepalive
                 follow_symlinks
+                port
+                host
             /,
         },
     );
-}
+};
 
 __PACKAGE__->meta->make_immutable;
-
 1;
 
 =head1 NAME
index 554026e..4209d62 100644 (file)
@@ -17,6 +17,7 @@ sub run {
 
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 NAME
 
index e5231e7..1874ea7 100644 (file)
@@ -62,7 +62,11 @@ sub run {
 }
 
 sub _application_args {
-    ()
+    my $self = shift;
+    return {
+        argv => $self->ARGV,
+        extra_argv => $self->extra_argv,
+    }
 }
 
 sub _plack_loader_args {
@@ -71,13 +75,15 @@ sub _plack_loader_args {
     return (port => $app_args[0]);
 }
 
+sub _plack_engine_name {}
+
 sub _run_application {
     my $self = shift;
     my $app = $self->application_name;
     Class::MOP::load_class($app);
     my $server;
-    if (my $e = $self->can('_plack_engine_name') ) {
-        $server = $self->load_engine($self->$e, $self->_plack_loader_args);
+    if (my $e = $self->_plack_engine_name ) {
+        $server = $self->load_engine($e, $self->_plack_loader_args);
     }
     else {
         $server = $self->autoload_engine($self->_plack_loader_args);
index f652e78..7619e61 100644 (file)
@@ -48,6 +48,7 @@ sub run {
 }
 
 __PACKAGE__->meta->make_immutable;
+1;
 
 =head1 NAME
 
index efea301..6f01812 100644 (file)
@@ -1113,6 +1113,19 @@ sub run_tests {
         ok( index($content, $path) > 1, 'uri can round trip through uri_for' )
             or diag("Expected $path, got $content");
     }
+
+    #
+    #   match_captures
+    #
+    {
+
+        ok( my $response = request('http://localhost/chained/match_captures/foo/bar'), 'match_captures: falling through' );
+        is($response->header('X-TestAppActionTestMatchCaptures'), 'fallthrough', 'match_captures: fell through');
+
+        ok($response = request('http://localhost/chained/match_captures/force/bar'), 'match_captures: *not* falling through' );
+        is($response->header('X-TestAppActionTestMatchCaptures'), 'forcing', 'match_captures: forced');
+        is($response->header('X-TestAppActionTestMatchCapturesHasRan'), 'yes', 'match_captures: actually ran');
+    }
 }
 
 done_testing;
index ffadb8a..697f2ad 100644 (file)
@@ -17,6 +17,8 @@ lives_ok {
 shift @TestAppToTestScripts::RUN_ARGS;
 my $server = pop @TestAppToTestScripts::RUN_ARGS;
 like ref($server), qr/^Plack::Handler/, 'Is a Plack::Handler';
-is_deeply \@TestAppToTestScripts::RUN_ARGS, [], "no args";
+is ref(delete($TestAppToTestScripts::RUN_ARGS[0]->{argv})), 'ARRAY';
+is ref(delete($TestAppToTestScripts::RUN_ARGS[0]->{extra_argv})), 'ARRAY';
+is_deeply \@TestAppToTestScripts::RUN_ARGS, [{}], "no args";
 
 done_testing;
index 2dde9d9..93f1c16 100644 (file)
@@ -37,8 +37,15 @@ sub testOption {
     } "new_with_options";
     # First element of RUN_ARGS will be the script name, which we don't care about
     shift @TestAppToTestScripts::RUN_ARGS;
+
     my $server = pop @TestAppToTestScripts::RUN_ARGS;
     is $server, $fake_handler, 'Loaded Plack handler gets passed to the app';
+
+    if (scalar(@TestAppToTestScripts::RUN_ARGS) && ref($TestAppToTestScripts::RUN_ARGS[-1]) eq "HASH") {
+        is ref(delete($TestAppToTestScripts::RUN_ARGS[-1]->{argv})), 'ARRAY';
+        is ref(delete($TestAppToTestScripts::RUN_ARGS[-1]->{extra_argv})), 'ARRAY';
+    }
+
     is_deeply \@TestAppToTestScripts::RUN_ARGS, $resultarray, "is_deeply comparison";
 }
 
diff --git a/t/aggregate/unit_core_script_run_options.t b/t/aggregate/unit_core_script_run_options.t
new file mode 100644 (file)
index 0000000..ff34a2c
--- /dev/null
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+use Test::More;
+use FindBin qw/$Bin/;
+use IO::Handle;
+use Try::Tiny;
+use File::Temp qw/ tempfile /;
+use lib "$Bin/../lib";
+
+use_ok('Catalyst::ScriptRunner');
+use_ok('ScriptTestApp');
+
+is ScriptTestApp->run_options, undef;
+
+my ($fh, $fn) = tempfile();
+
+binmode( $fh );
+binmode( STDOUT );
+
+local @ARGV = ();
+local %ENV;
+
+my $saved;
+open( $saved, '>&'. STDOUT->fileno )
+    or croak("Can't dup stdout: $!");
+open( STDOUT, '>&='. $fh->fileno )
+    or croak("Can't open stdout: $!");
+local $SIG{__WARN__} = sub {}; # Shut up warnings...
+try { Catalyst::ScriptRunner->run('ScriptTestApp', 'CGI'); pass("Ran ok") }
+catch { fail "Failed to run $_" };
+
+STDOUT->flush
+    or croak("Can't flush stdout: $!");
+
+open( STDOUT, '>&'. fileno($saved) )
+    or croak("Can't restore stdout: $!");
+
+is_deeply ScriptTestApp->run_options, { argv => [], extra_argv => [] };
+
+done_testing;
index 7cd6fb4..7121db4 100644 (file)
@@ -25,19 +25,19 @@ testOption( [ qw// ], ['3000', undef, opthash()] );
 # help           -? -help --help           -? --help
 # debug          -d -debug --debug         -d --debug
 # host           -host --host              --host
-testOption( [ qw/--host testhost/ ], ['3000', 'testhost', opthash()] );
-testOption( [ qw/-h testhost/ ], ['3000', 'testhost', opthash()] );
+testOption( [ qw/--host testhost/ ], ['3000', 'testhost', opthash(host => 'testhost')] );
+testOption( [ qw/-h testhost/ ], ['3000', 'testhost', opthash(host => 'testhost')] );
 
 # port           -p -port --port           -l --listen
-testOption( [ qw/-p 3001/ ], ['3001', undef, opthash()] );
-testOption( [ qw/--port 3001/ ], ['3001', undef, opthash()] );
+testOption( [ qw/-p 3001/ ], ['3001', undef, opthash(port => 3001)] );
+testOption( [ qw/--port 3001/ ], ['3001', undef, opthash(port => 3001)] );
 {
     local $ENV{TESTAPPTOTESTSCRIPTS_PORT} = 5000;
-    testOption( [ qw// ], [5000, undef, opthash()] );
+    testOption( [ qw// ], [5000, undef, opthash(port => 5000)] );
 }
 {
     local $ENV{CATALYST_PORT} = 5000;
-    testOption( [ qw// ], [5000, undef, opthash()] );
+    testOption( [ qw// ], [5000, undef, opthash(port => 5000)] );
 }
 
 if (try { require Starman; 1; }) {
@@ -127,9 +127,9 @@ sub testOption {
     $run_args[-1]->{pidfile} = $run_args[-1]->{pidfile}->file->stringify
       if scalar(@run_args) && $run_args[-1]->{pidfile};
 
-
     # Mangle argv into the options..
     $resultarray->[-1]->{argv} = $argstring;
+    $resultarray->[-1]->{extra_argv} = [];
     is_deeply \@run_args, $resultarray, "is_deeply comparison " . join(' ', @$argstring);
 }
 
@@ -190,6 +190,8 @@ sub opthash {
         'follow_symlinks' => 0,
         'background' => 0,
         'keepalive' => 0,
+        port => 3000,
+        host => undef,
         @_,
     };
 }
index d6430db..60520bd 100644 (file)
@@ -14,9 +14,10 @@ add_stopwords(qw(
     ctx _application MyApp restarter httponly Utils stash's unescapes
     dispatchtype dispatchtypes redispatch redispatching
     CaptureArgs ChainedParent PathPart PathPrefix
-    BUILDARGS metaclass namespaces pre
+    BUILDARGS metaclass namespaces pre ARGV ReverseProxy
     filename tempname request's subdirectory ini uninstalled uppercased
     wiki bitmask uri url urls dir hostname proxied http https IP SSL
+    inline INLINE plugins
 ));
 set_spell_cmd('aspell list -l en');
 all_pod_files_spelling_ok();
diff --git a/t/lib/ScriptTestApp.pm b/t/lib/ScriptTestApp.pm
new file mode 100644 (file)
index 0000000..c19a4e0
--- /dev/null
@@ -0,0 +1,8 @@
+package ScriptTestApp;
+use Moose;
+
+extends 'Catalyst';
+
+__PACKAGE__->setup;
+1;
+
diff --git a/t/lib/ScriptTestApp/Controller/Root.pm b/t/lib/ScriptTestApp/Controller/Root.pm
new file mode 100644 (file)
index 0000000..f88ce33
--- /dev/null
@@ -0,0 +1,10 @@
+package ScriptTestApp::Controller::Root;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller' }
+
+sub default : Chained('/') PathPart('') Args() {}
+
+1;
+
diff --git a/t/lib/TestApp/Action/TestMatchCaptures.pm b/t/lib/TestApp/Action/TestMatchCaptures.pm
new file mode 100644 (file)
index 0000000..2d9d167
--- /dev/null
@@ -0,0 +1,18 @@
+package TestApp::Action::TestMatchCaptures;
+
+use Moose;
+
+extends 'Catalyst::Action';
+
+sub match_captures {
+    my ($self, $c, $cap) = @_;
+    if ($cap->[0] eq 'force') {
+        $c->res->header( 'X-TestAppActionTestMatchCaptures', 'forcing' );
+        return 1;
+    } else {
+        $c->res->header( 'X-TestAppActionTestMatchCaptures', 'fallthrough' );
+        return 0;
+    }
+}
+
+1;
\ No newline at end of file
index a393e77..732a35b 100644 (file)
@@ -220,6 +220,13 @@ sub roundtrip_urifor_end : Chained('roundtrip_urifor') PathPart('') Args(1) {
     $c->stash->{no_end} = 1;
 }
 
+sub match_captures : Chained('/') PathPart('chained/match_captures') CaptureArgs(1) ActionClass('+TestApp::Action::TestMatchCaptures') {
+    my ($self, $c) = @_;
+    $c->res->header( 'X-TestAppActionTestMatchCapturesHasRan', 'yes');
+}
+
+sub match_captures_end : Chained('match_captures') PathPart('bar') Args(0) { }
+
 sub end :Private {
   my ($self, $c) = @_;
   return if $c->stash->{no_end};