1package Plack::Middleware::HTTPExceptions; 2use strict; 3use parent qw(Plack::Middleware); 4use Plack::Util::Accessor qw(rethrow); 5 6use Carp (); 7use Try::Tiny; 8use Scalar::Util 'blessed'; 9use HTTP::Status (); 10 11sub prepare_app { 12 my $self = shift; 13 $self->rethrow(1) if ($ENV{PLACK_ENV} || '') eq 'development'; 14} 15 16sub call { 17 my($self, $env) = @_; 18 19 my $res = try { 20 $self->app->($env); 21 } catch { 22 $self->transform_error($_, $env); 23 }; 24 25 return $res if ref $res eq 'ARRAY'; 26 27 return sub { 28 my $respond = shift; 29 30 my $writer; 31 try { 32 $res->(sub { return $writer = $respond->(@_) }); 33 } catch { 34 if ($writer) { 35 Carp::cluck $_; 36 $writer->close; 37 } else { 38 my $res = $self->transform_error($_, $env); 39 $respond->($res); 40 } 41 }; 42 }; 43} 44 45sub transform_error { 46 my($self, $e, $env) = @_; 47 48 my($code, $message); 49 if (blessed $e && $e->can('as_psgi')) { 50 return $e->as_psgi; 51 } 52 if (blessed $e && $e->can('code')) { 53 $code = $e->code; 54 $message = 55 $e->can('as_string') ? $e->as_string : 56 overload::Method($e, '""') ? "$e" : undef; 57 } else { 58 if ($self->rethrow) { 59 die $e; 60 } 61 else { 62 $code = 500; 63 $env->{'psgi.errors'}->print($e); 64 } 65 } 66 67 if ($code !~ /^[3-5]\d\d$/) { 68 die $e; # rethrow 69 } 70 71 $message ||= HTTP::Status::status_message($code); 72 73 my @headers = ( 74 'Content-Type' => 'text/plain', 75 'Content-Length' => length($message), 76 ); 77 78 if ($code =~ /^3/ && (my $loc = eval { $e->location })) { 79 push(@headers, Location => $loc); 80 } 81 82 return [ $code, \@headers, [ $message ] ]; 83} 84 851; 86 87__END__ 88 89=head1 NAME 90 91Plack::Middleware::HTTPExceptions - Catch HTTP exceptions 92 93=head1 SYNOPSIS 94 95 use HTTP::Exception; 96 97 my $app = sub { 98 # ... 99 HTTP::Exception::500->throw; 100 }; 101 102 builder { 103 enable "HTTPExceptions", rethrow => 1; 104 $app; 105 }; 106 107=head1 DESCRIPTION 108 109Plack::Middleware::HTTPExceptions is a PSGI middleware component to 110catch exceptions from applications that can be translated into HTTP 111status codes. 112 113Your application is supposed to throw an object that implements a 114C<code> method which returns the HTTP status code, such as 501 or 115404. This middleware catches them and creates a valid response out of 116the code. If the C<code> method returns a code that is not an HTTP 117redirect or error code (3xx, 4xx, or 5xx), the exception will be 118rethrown. 119 120The exception object may also implement C<as_string> or overload 121stringification to represent the text of the error. The text defaults to 122the status message of the error code, such as I<Service Unavailable> for 123C<503>. 124 125Finally, the exception object may implement C<as_psgi>, and the result 126of this will be returned directly as the PSGI response. 127 128If the code is in the 3xx range and the exception implements the 'location' 129method (HTTP::Exception::3xx does), the Location header will be set in the 130response, so you can do redirects this way. 131 132There are CPAN modules L<HTTP::Exception> and L<HTTP::Throwable>, and 133they are perfect to throw from your application to let this middleware 134catch and display, but you can also implement your own exception class 135to throw. 136 137If the thrown exception is not an object that implements either a 138C<code> or an C<as_psgi> method, a 500 error will be returned, and the 139exception is printed to the psgi.errors stream. 140Alternatively, you can pass a true value for the C<rethrow> parameter 141for this middleware, and the exception will instead be rethrown. This is 142enabled by default when C<PLACK_ENV> is set to C<development>, so that 143the L<StackTrace|Plack::Middleware::StackTrace> middleware can catch it 144instead. 145 146=head1 AUTHOR 147 148Tatsuhiko Miyagawa 149 150=head1 SEE ALSO 151 152paste.httpexceptions L<HTTP::Exception> L<HTTP::Throwable> 153 154=cut 155