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