Skip to content

Commit 30ad48a

Browse files
committed
Add new hook for exceptions in hooks (#1737)
This adds a new type of hook that is called in the event of an exception in a hook. Because of its nature it's a bit more complicated than a normal hook, in particular because it could result in recursive calling. Because this hook can be called in quite a variety of ways during response handling, and because the current hook exception behavior is quite blunt, I also wanted to make it quite flexible in terms of controlling what happens. In particular, I wanted to be able to change the default response in the event of an exception in a hook. The comments in the code and the documentation should all be self-explanatory in this regard.
1 parent 9b5ed02 commit 30ad48a

File tree

3 files changed

+114
-1
lines changed

3 files changed

+114
-1
lines changed

lib/Dancer2/Core/App.pm

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ sub supported_hooks {
771771
core.app.before_request
772772
core.app.after_request
773773
core.app.route_exception
774+
core.app.hook_exception
774775
core.app.before_file_render
775776
core.app.after_file_render
776777
core.error.before
@@ -790,6 +791,7 @@ sub hook_aliases {
790791
before_error => 'core.error.before',
791792
after_error => 'core.error.after',
792793
on_route_exception => 'core.app.route_exception',
794+
on_hook_exception => 'core.app.hook_exception',
793795

794796
before_file_render => 'core.app.before_file_render',
795797
after_file_render => 'core.app.after_file_render',
@@ -1208,7 +1210,24 @@ sub compile_hooks {
12081210
eval { $EVAL_SHIM->($hook,@_); 1; }
12091211
or do {
12101212
my $err = $@ || "Zombie Error";
1211-
$app->cleanup;
1213+
# Don't execute the hook_exception hook if the exception
1214+
# has been generated from a hook exception handler itself,
1215+
# thus preventing potentially recursive code.
1216+
$self->execute_hook( 'core.app.hook_exception', $app, $err )
1217+
unless $position eq 'core.app.hook_exception';
1218+
my $is_halted = $app->response->is_halted; # Capture before cleanup
1219+
# We can't cleanup if we're in the hook for a hook
1220+
# exception, as this would clear the custom response that
1221+
# may have been set by the hook. However, there is no need
1222+
# to do so, as the upper hook that called this hook
1223+
# exception will perform the cleanup instead anyway
1224+
$app->cleanup unless $position eq 'core.app.hook_exception';
1225+
# Allow the hook function to halt the response, thus
1226+
# retaining any response it may have set. Otherwise the
1227+
# croak from this function will overwrite any content that
1228+
# may have been set by the hook
1229+
return if $is_halted;
1230+
# Default behavior if nothing else defined
12121231
$app->log('error', "Exception caught in '$position' filter: $err");
12131232
croak "Exception caught in '$position' filter: $err";
12141233
};

lib/Dancer2/Manual.pod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ for details about the following hooks:
667667

668668
=item * C<on_route_exception>
669669

670+
=item * C<on_hook_exception>
671+
670672
=back
671673

672674
=head2 File Rendering
@@ -925,6 +927,21 @@ L<Dancer2::Core::App> and the error as arguments.
925927
my ($app, $error) = @_;
926928
};
927929

930+
C<on_hook_exception> is called when an exception has been caught within a hook,
931+
just before logging and rethrowing it higher. This hook receives a
932+
L<Dancer2::Core::App> and the error as arguments. If the function provided to
933+
on_hook_exception causes an exception itself, then this will be ignored (thus
934+
preventing a potentially recursive situation). However, it is still possible
935+
for the function to set a custom response and halt it (and optionally die),
936+
thus providing a method to render custom content in the event of an exception
937+
in a hook. This is shown in the example below:
938+
939+
hook on_hook_exception => sub {
940+
my ($app, $error) = @_;
941+
$app->response->content("Some custom content for the response");
942+
$app->response->halt;
943+
};
944+
928945
=head1 SESSIONS
929946

930947
=head2 Handling sessions

t/hooks.t

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,60 @@ my $tests_flags = {};
157157
};
158158
}
159159

160+
# 3 test apps for the exception hook - see comments below
161+
{
162+
package App::HookException;
163+
use Dancer2;
164+
165+
get '/hook_exception' => sub { return 1 };
166+
167+
hook after => sub {
168+
die 'this is an exception in the after hook';
169+
};
170+
171+
hook on_hook_exception => sub {
172+
my ($app, $error) = @_;
173+
::is ref($app), 'Dancer2::Core::App';
174+
::like $error, qr/this is an exception in the after hook/;
175+
$app->response->content("Setting response from exception hook");
176+
$app->response->halt;
177+
};
178+
}
179+
180+
{
181+
package App::HookExceptionDouble;
182+
use Dancer2;
183+
184+
get '/hook_exception_double' => sub { return 1 };
185+
186+
hook after => sub {
187+
die 'this is an exception in the after hook';
188+
};
189+
190+
hook on_hook_exception => sub {
191+
my ($app, $error) = @_;
192+
$app->response->content("Setting response from exception hook and then dying");
193+
$app->response->halt;
194+
die 'this is an exception in the exception hook';
195+
};
196+
}
197+
198+
{
199+
package App::HookExceptionRecursive;
200+
use Dancer2;
201+
202+
get '/hook_exception_recursive' => sub { return 1 };
203+
204+
hook after => sub {
205+
die 'this is an exception in the after hook';
206+
};
207+
208+
hook on_hook_exception => sub {
209+
my ($app, $error) = @_;
210+
die 'this is an exception in the hook exception causing recursion';
211+
};
212+
}
213+
160214
subtest 'Request hooks' => sub {
161215
my $test = Plack::Test->create( App::WithSerializer->to_app );
162216
$test->request( GET '/' );
@@ -234,4 +288,27 @@ subtest 'route_exception' => sub {
234288
capture_stderr { $test->request( GET '/route_exception' ) };
235289
};
236290

291+
# A basic test for a hook that is called in the event of an exception in a hook
292+
subtest 'hook_exception' => sub {
293+
my $test = Plack::Test->create( App::HookException->to_app );
294+
my $resp = $test->request( GET '/hook_exception' );
295+
is( $resp->content, 'Setting response from exception hook' );
296+
};
297+
298+
# A more advanced test that checks that an exception can take place within the
299+
# exception hook, but only if it sets the response content and performs a halt
300+
subtest 'hook_exception_double' => sub {
301+
my $test = Plack::Test->create( App::HookExceptionDouble->to_app );
302+
my $resp = $test->request( GET '/hook_exception_double');
303+
is( $resp->content, 'Setting response from exception hook and then dying' );
304+
};
305+
306+
# Similar to the above, but without any handling for the second exception which
307+
# would otherwise result in a recursive situation
308+
subtest 'hook_exception_recursive' => sub {
309+
my $test = Plack::Test->create( App::HookExceptionRecursive->to_app );
310+
my $resp = $test->request( GET '/hook_exception_recursive');
311+
like( $resp->content, qr/Internal Server Error/ );
312+
};
313+
237314
done_testing;

0 commit comments

Comments
 (0)