File Coverage

blib/lib/Plack/Middleware/PrettyException.pm
Criterion Covered Total %
statement 104 111 93.6
branch 33 40 82.5
condition 23 30 76.6
subroutine 14 14 100.0
pod 1 2 50.0
total 175 197 88.8


line stmt bran cond sub pod time code
1             package Plack::Middleware::PrettyException;
2              
3             # ABSTRACT: Capture exceptions and present them as HTML or JSON
4              
5             our $VERSION = '1.010'; # VERSION
6              
7 1     1   716 use 5.010;
  1         4  
8 1     1   5 use strict;
  1         4  
  1         21  
9 1     1   5 use warnings;
  1         2  
  1         28  
10 1     1   5 use parent qw(Plack::Middleware);
  1         2  
  1         6  
11 1     1   68 use Plack::Util;
  1         2  
  1         40  
12 1     1   6 use Plack::Util::Accessor qw(force_json);
  1         3  
  1         8  
13 1     1   54 use HTTP::Headers;
  1         18  
  1         35  
14 1     1   6 use JSON::MaybeXS qw(encode_json);
  1         3  
  1         64  
15 1     1   500 use HTTP::Status qw(is_error);
  1         5006  
  1         120  
16 1     1   9 use Scalar::Util 'blessed';
  1         3  
  1         48  
17 1     1   452 use Log::Any qw($log);
  1         8218  
  1         5  
18              
19             sub call {
20 17     17 1 100189 my $self = shift;
21 17         33 my $env = shift;
22              
23 17         55 my $r;
24             my $error;
25 17         0 my $exception;
26 17         26 my $died = 0;
27             eval {
28 17         58 $r = $self->app->($env);
29 7         156 1;
30 17 100       30 } or do {
31 10         147058 my $e = $@;
32 10         21 $died = 1;
33 10 100       43 if ( blessed($e) ) {
34 8         17 $exception = $e;
35 8 50       82 if ( $e->can('message') ) {
36 8         29 $error = $e->message;
37             }
38             else {
39 0         0 $error = '' . $e;
40             }
41 8 50       98 $r->[0] =
    100          
42             $e->can('status_code') ? $e->status_code
43             : $e->can('http_status') ? $e->http_status
44             : 500;
45 8   100     47 $r->[0] ||= 500;
46              
47 8 100 66     45 if ( $r->[0] =~ /^3/ && $e->can('location') ) {
48 1         3 push( @{ $r->[1] }, Location => $e->location );
  1         6  
49 1 50       7 push( @{ $r->[2] }, $e->location ) unless $r->[2];
  1         3  
50             }
51              
52             }
53             else {
54 2         6 $r->[0] = 500;
55 2         4 $error = $e;
56             }
57             };
58              
59             return Plack::Util::response_cb(
60             $r,
61             sub {
62 17     17   227 my $r = shift;
63              
64 17 100 100     56 if ( !$died && !is_error( $r->[0] ) ) {
65              
66             # all is ok!
67 2         19 return;
68             }
69 15 100       87 if ( $r->[0] =~ /^3/ ) {
70              
71             # it's a redirect
72 1         3 return;
73             }
74              
75             # there was an error!
76              
77 14 100       32 unless ($error) {
78 7   100     21 my $body = $r->[2] || 'error not found in body';
79 7 100       29 $error = ref($body) eq 'ARRAY' ? join( '', @$body ) : $body;
80             }
81              
82             my $location = join( '',
83 14         31 map { $env->{$_} } qw(HTTP_HOST SCRIPT_NAME PATH_INFO) );
  42         122  
84 14         82 $log->error( $location . ': ' . $error );
85              
86 14         60 my $orig_headers = HTTP::Headers->new( @{ $r->[1] } );
  14         87  
87 14         343 my $err_headers = Plack::Util::headers( [] );
88 14         309 my $err_body;
89              
90             # it already is JSON, so return that
91 14 100       51 if ( $orig_headers->content_type =~ m{application/json}i ) {
92 2         59 return;
93             }
94              
95             # force json, or client requested JSON, so render errors as JSON
96 12 100 66     179 if ($self->force_json
      100        
97             || ( exists $env->{HTTP_ACCEPT}
98             && $env->{HTTP_ACCEPT} =~ m{application/json}i )
99             ) {
100 4         94 $err_headers->set( 'content-type' => 'application/json' );
101 4         85 my $err_payload = { status => 'error', message => "" . $error };
102 4 50 66     19 if ($exception && $exception->can('does')) {
103 0 0       0 if ($exception->does('Throwable::X')) {
104 0         0 my $payload = $exception->payload;
105 0         0 while (my ($k, $v) = each %$payload) {
106 0         0 $err_payload->{$k} = $v;
107             }
108 0         0 $err_payload->{ident} = $exception->ident;
109             }
110             }
111              
112 4         40 $err_body = encode_json( $err_payload );
113             }
114              
115             # return HTML as default
116             else {
117 8         118 $err_headers->set(
118             'content-type' => 'text/html;charset=utf-8' );
119 8         258 $err_body = $self->render_html_error( $r->[0], $error, $exception, $env );
120             }
121 12         59 $r->[1] = $err_headers->headers;
122 12         172 $r->[2] = [$err_body];
123 12         115 return;
124             }
125 17         124 );
126             }
127              
128             sub render_html_error {
129 8     8 0 27 my ( $self, $status, $error, $exception, $env ) = @_;
130              
131 8   50     19 $status ||= 'unknown HTTP status code';
132 8   50     20 $error ||= 'unknown error';
133              
134 8         14 my $more='';
135 8 100 100     57 if ($exception && $exception->can('does')) {
136 3         50 my @more;
137 3 100       11 if ($exception->does('Throwable::X')) {
138 2         14 push(@more, "
  • ".$exception->ident."
  • ");
    139 2   50     16 push(@more, "
  • ".($exception->message || 'unknown exception message')."
  • ");
    140 2         18 my $payload = $exception->payload;
    141 2         18 while (my ($k, $v) = each %$payload) {
    142 2 100       8 if (ref($v)) {
    143 1 50       5 if (ref($v) eq 'ARRAY') {
    144 1         6 push(@more,sprintf("
  • %s:
      ", $k ));
  • 145 1         4 foreach my $sv (@$v) {
    146 3         10 push(@more,sprintf("
  • %s
  • ", $sv ));
    147             }
    148 1         12 push(@more,sprintf(""));
    149             }
    150             else {
    151 0         0 push(@more,sprintf("
  • %s which is an unhandled ref of %s
  • ", $k, ref($v)));
    152             }
    153             }
    154             else {
    155 1   50     10 push(@more,sprintf("
  • %s: %s
  • ", $k, $v // ''));
    156             }
    157             }
    158             }
    159 3 100       62 if (@more) {
    160 2         14 $more='
      '.join("\n",@more).'
    ';
    161             }
    162             }
    163              
    164 8         38 return <<"UGLYERROR";
    165            
    166             Error $status
    167            
    168            

    Error $status

    169            

    $error

    170             $more
    171            
    172            
    173             UGLYERROR
    174             }
    175              
    176             1;
    177              
    178             __END__