File Coverage

blib/lib/Plack/Middleware/StackTrace.pm
Criterion Covered Total %
statement 64 65 98.4
branch 15 18 83.3
condition 11 14 78.5
subroutine 15 15 100.0
pod 1 4 25.0
total 106 116 91.3


line stmt bran cond sub pod time code
1             package Plack::Middleware::StackTrace;
2 8     8   789811 use strict;
  8         16  
  8         306  
3 8     8   35 use warnings;
  8         16  
  8         487  
4 8     8   2744 use parent qw/Plack::Middleware/;
  8         2061  
  8         52  
5 8     8   4225 use Devel::StackTrace;
  8         25367  
  8         277  
6 8     8   3513 use Devel::StackTrace::AsHTML;
  8         76888  
  8         409  
7 8     8   56 use Scalar::Util qw( refaddr );
  8         15  
  8         459  
8 8     8   4303 use Try::Tiny;
  8         18648  
  8         596  
9 8     8   57 use Plack::Util::Accessor qw( force no_print_errors );
  8         15  
  8         65  
10              
11             our $StackTraceClass = "Devel::StackTrace";
12              
13             # Optional since it needs PadWalker
14             if (try { require Devel::StackTrace::WithLexicals; Devel::StackTrace::WithLexicals->VERSION(0.08); 1 }) {
15             $StackTraceClass = "Devel::StackTrace::WithLexicals";
16             }
17              
18             sub call {
19 10     10 1 31 my($self, $env) = @_;
20              
21 10         23 my ($trace, %string_traces, %ref_traces);
22             local $SIG{__DIE__} = sub {
23 14     14   1060 $trace = $StackTraceClass->new(
24             indent => 1, message => munge_error($_[0], [ caller ]),
25             ignore_package => __PACKAGE__, no_refs => 1,
26             );
27 14 100       13185 if (ref $_[0]) {
28 4   66     41 $ref_traces{refaddr($_[0])} ||= $trace;
29             }
30             else {
31 10   66     114 $string_traces{$_[0]} ||= $trace;
32             }
33 14         257 die @_;
34 10         114 };
35              
36 10         35 my $caught;
37             my $res = try {
38 10     10   553 $self->app->($env);
39             } catch {
40 7     7   178 $caught = $_;
41 7         34 [ 500, [ "Content-Type", "text/plain; charset=utf-8" ], [ no_trace_error(utf8_safe($caught)) ] ];
42 10         119 };
43              
44 10 100       221 if ($caught) {
45             # Try to find the correct trace for the caught exception
46 7         26 my $caught_trace;
47 7 100       38 if (ref $caught) {
48 2         7 $caught_trace = $ref_traces{refaddr($caught)};
49             }
50             else {
51             # This is not guaranteed to work if multiple exceptions with
52             # the same message are thrown.
53 5         13 $caught_trace = $string_traces{$caught};
54             }
55 7 50       26 $trace = $caught_trace if $caught_trace;
56             }
57              
58             # Use ref $trace to avoid overloaded as_string() for bool evaluation
59 10 50 100     171 if (ref $trace && ($caught || ($self->force && ref $res eq 'ARRAY' && $res->[0] == 500)) ) {
      66        
60 8         47 my $text = $trace->as_string;
61 8         15430 my $html = $trace->as_html;
62 8         80365 $env->{'plack.stacktrace.text'} = $text;
63 8         33 $env->{'plack.stacktrace.html'} = $html;
64 8 100       54 $env->{'psgi.errors'}->print($text) unless $self->no_print_errors;
65 8 100 100     85 if (($env->{HTTP_ACCEPT} || '*/*') =~ /html/) {
66 1         7 $res = [500, ['Content-Type' => 'text/html; charset=utf-8'], [ utf8_safe($html) ]];
67             } else {
68 7         38 $res = [500, ['Content-Type' => 'text/plain; charset=utf-8'], [ utf8_safe($text) ]];
69             }
70             }
71              
72             # break $trace here since $SIG{__DIE__} holds the ref to it, and
73             # $trace has refs to Standalone.pm's args ($conn etc.) and
74             # prevents garbage collection to be happening.
75 10         31 undef $trace;
76              
77 10         682 return $res;
78             }
79              
80             sub no_trace_error {
81 7     7 0 16 my $msg = shift;
82 7         50 chomp($msg);
83              
84 7         51 return <
85             The application raised the following error:
86              
87             $msg
88              
89             and the StackTrace middleware couldn't catch its stack trace, possibly because your application overrides \$SIG{__DIE__} by itself, preventing the middleware from working correctly. Remove the offending code or module that does it: known examples are CGI::Carp and Carp::Always.
90             EOF
91             }
92              
93             sub munge_error {
94 14     14 0 61 my($err, $caller) = @_;
95 14 100       64 return $err if ref $err;
96              
97             # Ugly hack to remove " at ... line ..." automatically appended by perl
98             # If there's a proper way to do this, please let me know.
99 10         289 $err =~ s/ at \Q$caller->[1]\E line $caller->[2]\.\n$//;
100              
101 10         119 return $err;
102             }
103              
104             sub utf8_safe {
105 15     15 0 36 my $str = shift;
106              
107             # NOTE: I know messing with utf8:: in the code is WRONG, but
108             # because we're running someone else's code that we can't
109             # guarantee which encoding an exception is encoded, there's no
110             # better way than doing this. The latest Devel::StackTrace::AsHTML
111             # (0.08 or later) encodes high-bit chars as HTML entities, so this
112             # path won't be executed.
113 15 50       69 if (utf8::is_utf8($str)) {
114 0         0 utf8::encode($str);
115             }
116              
117 15         90 $str;
118             }
119              
120             1;
121              
122             __END__