File Coverage

blib/lib/Plack/Middleware/BetterStackTrace.pm
Criterion Covered Total %
statement 126 129 97.6
branch 23 32 71.8
condition 9 15 60.0
subroutine 26 26 100.0
pod 1 10 10.0
total 185 212 87.2


line stmt bran cond sub pod time code
1             # vim: set expandtab ts=4 sw=4 nowrap ft=perl ff=unix :
2             package Plack::Middleware::BetterStackTrace;
3 5     5   130208 use 5.008005;
  5         22  
  5         316  
4 5     5   29 use strict;
  5         12  
  5         193  
5 5     5   29 use warnings;
  5         19  
  5         601  
6              
7             our $VERSION = "0.02";
8              
9 5     5   5704 use parent qw/Plack::Middleware/;
  5         1894  
  5         29  
10              
11 5     5   125040 use JSON;
  5         67374  
  5         35  
12 5     5   7276 use Try::Tiny;
  5         12734  
  5         318  
13 5     5   18319 use Data::Dumper;
  5         74564  
  5         423  
14 5     5   24779 use Text::Xslate;
  5         126059  
  5         562  
15 5     5   9810 use Plack::Request;
  5         468916  
  5         221  
16 5     5   11572 use Term::ANSIColor;
  5         62442  
  5         700  
17 5     5   69 use Text::Xslate::Util;
  5         9  
  5         290  
18 5     5   9374 use Devel::StackTrace::WithLexicals;
  5         59400  
  5         214  
19 5     5   56 use Plack::Util::Accessor qw( force no_print_errors );
  5         10  
  5         68  
20              
21             my $dumper = sub {
22             my $value = shift;
23             $value = $$value if ref $value eq 'SCALAR' or ref $value eq 'REF';
24             my $d = Data::Dumper->new([$value]);
25             $d->Indent(1)->Terse(1)->Deparse(1);
26             chomp(my $dump = $d->Dump);
27             $dump;
28             };
29              
30             sub call {
31 4     4 1 108679 my ($self, $env) = @_;
32              
33 4         11 my $trace;
34             local $SIG{__DIE__} = sub {
35 4     4   1290 $| = 1;
36 4         31 $trace = Devel::StackTrace::WithLexicals->new(
37             indent => 1, message => munge_error($_[0], [caller]),
38             ignore_package => __PACKAGE__,
39             );
40 4         3096 die @_;
41 4         42 };
42              
43 4         10 my $caught;
44             my $res = try {
45 4     4   461 $self->app->($env);
46             }
47             catch {
48 1     1   12 $caught = $_;
49             [
50 1         6 500, [ "Content-Type", "text/plain; charset=utf-8" ],
51             [ no_trace_error(utf8_safe($caught)) ]
52             ];
53 4         39 };
54              
55 4 50 66     83 if (
      33        
56             $trace
57             && ($caught
58             || ($self->force && ref $res eq 'ARRAY' && $res->[0] == 500))
59             ) {
60 2   50     5249 my $text = render_text(
61             $trace, $env,
62             application_caller_subroutine =>
63             $self->{application_caller_subroutine} || '',
64             );
65 2   50     24 my $html = render_html(
66             $trace, $env,
67             application_caller_subroutine =>
68             $self->{application_caller_subroutine} || '',
69             );
70 2         5507 $env->{'plack.stacktrace.text'} = $text;
71 2         230 $env->{'plack.stacktrace.html'} = $html;
72 2 50       19 $env->{'psgi.errors'}->print($text) unless $self->no_print_errors;
73 2   100     45 my $accept_mime_types = $env->{HTTP_ACCEPT} || '*/*';
74 2 100       14 if ($accept_mime_types =~ /html/) {
75 1         8 $res = [
76             500, [ 'Content-Type' => 'text/html; charset=utf-8' ],
77             [ utf8_safe($html) ]
78             ];
79             } else {
80 1         6 $res = [
81             500, [ 'Content-Type' => 'text/plain; charset=utf-8' ],
82             [ utf8_safe($text) ]
83             ];
84             }
85             } ## end if ($trace && ($caught || ($self...)))
86              
87             # break $trace here since $SIG{__DIE__} holds the ref to it, and
88             # $trace has refs to Standalone.pm's args ($conn etc.) and
89             # prevents garbage collection to be happening.
90 4         6173 undef $trace;
91              
92 4         397 return $res;
93             } ## end sub call
94              
95             sub no_trace_error {
96 1     1 0 3 my $msg = shift;
97 1         3 chomp($msg);
98              
99 1         15 return <<EOF;
100             The application raised the following error:
101              
102             $msg
103              
104             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.
105             EOF
106             }
107              
108             sub munge_error {
109 4     4 0 10 my ($err, $caller) = @_;
110 4 50       25 return $err if ref $err;
111              
112             # Ugly hack to remove " at ... line ..." automatically appended by perl
113             # If there's a proper way to do this, please let me know.
114 4         95 $err =~ s/ at \Q$caller->[1]\E line $caller->[2]\.\n$//;
115              
116 4         74 return $err;
117             }
118              
119             sub utf8_safe {
120 3     3 0 108 my $str = shift;
121              
122             # NOTE: I know messing with utf8:: in the code is WRONG, but
123             # because we're running someone else's code that we can't
124             # guarnatee which encoding an exception is encoded, there's no
125             # better way than doing this. The latest Devel::StackTrace::AsHTML
126             # (0.08 or later) encodes high-bit chars as HTML entities, so this
127             # path won't be executed.
128 3 50       22 if (utf8::is_utf8($str)) {
129 0         0 utf8::encode($str);
130             }
131              
132 3         50 $str;
133             }
134              
135             sub frame_filter {
136 2     2 0 8 my ($trace, $tx, %opt) = @_;
137              
138 2         11 my @frames = $trace->frames();
139 2         23 my @filtered_frames;
140              
141 2         4 my $context = 'application';
142              
143 2         8 for my $i (0 .. $#frames) {
144 26         2447 my $frame = $frames[$i];
145 26 100       128 my $next_frame = ($i == $#frames - 1) ? undef : $frames[ $i + 1 ];
146              
147 26         145 my @method_name = split('::', $frame->subroutine);
148 26         323 my $method_name = pop(@method_name);
149 26         76 my $module_name = join('::', @method_name);
150              
151 26 100       79 if ($module_name) {
152 21         118 $method_name = '::' . $method_name;
153             }
154              
155 26 50 66     189 if ( $next_frame
156             && $next_frame->subroutine eq $opt{application_caller_subroutine}) {
157 0         0 $context = 'dunno';
158             }
159              
160 26 100       531 my @args = $next_frame ? $next_frame->args : undef;
161              
162 26         187 push @filtered_frames,
163             +{
164             context => $context,
165             subroutine => $frame->subroutine,
166             module_name => $module_name,
167             method_name => $method_name,
168             filename => $frame->filename,
169             line => $frame->line,
170             info_html => $tx->render(
171             'variables_info',
172             +{
173             frame => $frame,
174             args => \@args,
175             html_formatted_code_block => context_html($frame),
176             }
177             ),
178             };
179             } ## end for my $i (0 .. $#frames)
180              
181 2         16 \@filtered_frames;
182             } ## end sub frame_filter
183              
184             sub context_html {
185 26     26 0 320 my $frame = shift;
186 26         69 my $file = $frame->filename;
187 26         143 my $linenum = $frame->line;
188 26         195 my $code = '<div class="code">';
189 26 50       1617 if (-f $file) {
190 26         57 my $start = $linenum - 3;
191 26         41 my $end = $linenum + 3;
192 26 50       73 $start = $start < 1 ? 1 : $start;
193 26 50       1924 open my $fh, '<', $file
194             or die "cannot open $file:$!";
195 26         46 my $cur_line = 0;
196 26         842 while (my $line = <$fh>) {
197 1236         1412 ++$cur_line;
198 1236 100       2486 last if $cur_line > $end;
199 1212 100       4463 next if $cur_line < $start;
200 180         414 $line =~ s|\t| |g;
201 180 100       531 my @tag =
202             $cur_line == $linenum
203             ? (q{<pre class="highlight">}, '</pre>')
204             : ('<pre>', '</pre>');
205 180         2738 $code .= sprintf(
206             '%s%5d: %s%s', $tag[0], $cur_line,
207             Text::Xslate::Util::html_escape($line),
208             $tag[1],
209             );
210             }
211 26         962 close $file;
212             }
213 26         49 $code .= '</div>';
214 26         543 $code;
215             } ## end sub context_html
216              
217             sub render_text {
218 2     2 0 37 my ($trace, $psgi_env, %opt) = @_;
219              
220 2         6 my $text = '';
221 2         6 my $first = 1;
222 2         9 my @frames = $trace->frames;
223 2         26 for my $frame ($trace->frames()) {
224 26 50       84 if ($frame->subroutine eq $opt{application_caller_subroutine}) {
225 0         0 last;
226             }
227              
228 26         173 $text .= $frame->as_string($first, \%opt) . "\n";
229 26         1609 $first = 0;
230             }
231 2         21 $text;
232             }
233              
234             sub render_html {
235 2     2 0 7 my ($trace, $psgi_env, %opt) = @_;
236              
237 2         20 my $message = $trace->frame(0)->as_string(1);
238              
239             # Remove escape sequence
240 2         72 $message = Term::ANSIColor::colorstrip($message);
241              
242 2         30 my $tx = Text::Xslate->new(
243             syntax => 'TTerse',
244             path => +{
245             base => base_html(),
246             variables_info => variables_info_html(),
247             },
248             function => {
249             dump => $dumper,
250             }
251             );
252              
253 2         1063 my $request = Plack::Request->new($psgi_env);
254 2         30 my $backtrace_frames = frame_filter($trace, $tx, %opt);
255              
256 2         33 return $tx->render(
257             'base',
258             +{
259             backtrace_frames => $backtrace_frames,
260             request => $request,
261             message => $message,
262             }
263             );
264             } ## end sub render_html
265              
266             sub base_html {
267 2     2 0 11 <<'EOTMPL' }
268             <!DOCTYPE html>
269             <html>
270             <head>
271             <title>[% message %][% IF request.request_uri %] at [% request.request_uri %][% END %]</title>
272             <style>
273             /* Basic reset */
274             * {
275             margin: 0;
276             padding: 0;
277             }
278              
279             table {
280             width: 100%;
281             border-collapse: collapse;
282             }
283              
284             th, td {
285             vertical-align: top;
286             text-align: left;
287             }
288              
289             textarea {
290             resize: none;
291             }
292              
293             body {
294             font-size: 10pt;
295             }
296              
297             body, td, input, textarea {
298             font-family: helvetica neue, lucida grande, sans-serif;
299             line-height: 1.5;
300             color: #333;
301             text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
302             }
303              
304             html {
305             background: #f0f0f5;
306             }
307              
308             /* ---------------------------------------------------------------------
309             * Basic layout
310             * --------------------------------------------------------------------- */
311              
312             /* Small */
313             @media screen and (max-width: 1100px) {
314             html {
315             overflow-y: scroll;
316             }
317              
318             body {
319             margin: 0 20px;
320             }
321              
322             header.exception {
323             margin: 0 -20px;
324             }
325              
326             nav.sidebar {
327             padding: 0;
328             margin: 20px 0;
329             }
330              
331             ul.frames {
332             max-height: 200px;
333             overflow: auto;
334             }
335             }
336              
337             /* Wide */
338             @media screen and (min-width: 1100px) {
339             header.exception {
340             position: fixed;
341             top: 0;
342             left: 0;
343             right: 0;
344             }
345              
346             nav.sidebar,
347             .frame_info {
348             position: fixed;
349             top: 95px;
350             bottom: 0;
351              
352             box-sizing: border-box;
353              
354             overflow-y: auto;
355             overflow-x: hidden;
356             }
357              
358             nav.sidebar {
359             width: 40%;
360             left: 20px;
361             top: 115px;
362             bottom: 20px;
363             }
364              
365             .frame_info {
366             right: 0;
367             left: 40%;
368              
369             padding: 20px;
370             padding-left: 10px;
371             margin-left: 30px;
372             }
373             }
374              
375             nav.sidebar {
376             background: #d3d3da;
377             border-top: solid 3px #a33;
378             border-bottom: solid 3px #a33;
379             border-radius: 4px;
380             box-shadow: 0 0 6px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
381             }
382              
383             /* ---------------------------------------------------------------------
384             * Header
385             * --------------------------------------------------------------------- */
386              
387             header.exception {
388             padding: 18px 20px;
389              
390             height: 59px;
391             min-height: 59px;
392              
393             overflow: hidden;
394              
395             background-color: #20202a;
396             color: #aaa;
397             text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
398             font-weight: 200;
399             box-shadow: inset 0 -5px 3px -3px rgba(0, 0, 0, 0.05), inset 0 -1px 0 rgba(0, 0, 0, 0.05);
400              
401             -webkit-text-smoothing: antialiased;
402             }
403              
404             /* Heading */
405             header.exception h2 {
406             font-weight: 200;
407             font-size: 11pt;
408             }
409              
410             header.exception h2,
411             header.exception p {
412             line-height: 1.4em;
413             overflow: hidden;
414             white-space: pre;
415             text-overflow: ellipsis;
416             }
417              
418             header.exception h2 strong {
419             font-weight: 700;
420             color: #d55;
421             }
422              
423             header.exception p {
424             font-weight: 200;
425             font-size: 20pt;
426             color: white;
427             }
428              
429             header.exception:hover {
430             height: auto;
431             z-index: 2;
432             }
433              
434             header.exception:hover h2,
435             header.exception:hover p {
436             padding-right: 20px;
437             overflow-y: auto;
438             word-wrap: break-word;
439             height: auto;
440             max-height: 7em;
441             }
442              
443             @media screen and (max-width: 1100px) {
444             header.exception {
445             height: auto;
446             }
447              
448             header.exception h2,
449             header.exception p {
450             padding-right: 20px;
451             overflow-y: auto;
452             word-wrap: break-word;
453             height: auto;
454             max-height: 7em;
455             }
456             }
457              
458             /* ---------------------------------------------------------------------
459             * Navigation
460             * --------------------------------------------------------------------- */
461              
462             nav.tabs {
463             border-bottom: solid 1px #ddd;
464              
465             background-color: #eee;
466             text-align: center;
467              
468             padding: 6px;
469              
470             box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
471             }
472              
473             nav.tabs a {
474             display: inline-block;
475              
476             height: 22px;
477             line-height: 22px;
478             padding: 0 10px;
479              
480             text-decoration: none;
481             font-size: 8pt;
482             font-weight: bold;
483              
484             color: #999;
485             text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
486             }
487              
488             nav.tabs a.selected {
489             color: white;
490             background: rgba(0, 0, 0, 0.5);
491             border-radius: 16px;
492             box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.1);
493             text-shadow: 0 0 4px rgba(0, 0, 0, 0.4), 0 1px 0 rgba(0, 0, 0, 0.4);
494             }
495              
496             /* ---------------------------------------------------------------------
497             * Sidebar
498             * --------------------------------------------------------------------- */
499              
500             ul.frames {
501             box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
502             }
503              
504             /* Each item */
505             ul.frames li {
506             background-color: #f8f8f8;
507             background: -webkit-linear-gradient(top, #f8f8f8 80%, #f0f0f0);
508             background: -moz-linear-gradient(top, #f8f8f8 80%, #f0f0f0);
509             background: linear-gradient(top, #f8f8f8 80%, #f0f0f0);
510             box-shadow: inset 0 -1px 0 #e2e2e2;
511             padding: 7px 20px;
512              
513             cursor: pointer;
514             overflow: hidden;
515             }
516              
517             ul.frames .name,
518             ul.frames .location {
519             overflow: hidden;
520             height: 1.5em;
521              
522             white-space: nowrap;
523             word-wrap: none;
524             text-overflow: ellipsis;
525             }
526              
527             ul.frames .method {
528             color: #966;
529             }
530              
531             ul.frames .location {
532             font-size: 0.85em;
533             font-weight: 400;
534             color: #999;
535             }
536              
537             ul.frames .line {
538             font-weight: bold;
539             }
540              
541             /* Selected frame */
542             ul.frames li.selected {
543             background: #38a;
544             box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), inset 0 2px 0 rgba(255, 255, 255, 0.01), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
545             }
546              
547             ul.frames li.selected .name,
548             ul.frames li.selected .method,
549             ul.frames li.selected .location {
550             color: white;
551             text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
552             }
553              
554             ul.frames li.selected .location {
555             opacity: 0.6;
556             }
557              
558             /* Iconography */
559             ul.frames li {
560             padding-left: 60px;
561             position: relative;
562             }
563              
564             ul.frames li .icon {
565             display: block;
566             width: 20px;
567             height: 20px;
568             line-height: 20px;
569             border-radius: 15px;
570              
571             text-align: center;
572              
573             background: white;
574             border: solid 2px #ccc;
575              
576             font-size: 9pt;
577             font-weight: 200;
578             font-style: normal;
579              
580             position: absolute;
581             top: 14px;
582             left: 20px;
583             }
584              
585             ul.frames .icon.application {
586             background: #808090;
587             border-color: #555;
588             }
589              
590             ul.frames .icon.application:before {
591             content: 'A';
592             color: white;
593             text-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
594             }
595              
596             /* Responsiveness -- flow to single-line mode */
597             @media screen and (max-width: 1100px) {
598             ul.frames li {
599             padding-top: 6px;
600             padding-bottom: 6px;
601             padding-left: 36px;
602             line-height: 1.3;
603             }
604              
605             ul.frames li .icon {
606             width: 11px;
607             height: 11px;
608             line-height: 11px;
609              
610             top: 7px;
611             left: 10px;
612             font-size: 5pt;
613             }
614              
615             ul.frames .name,
616             ul.frames .location {
617             display: inline-block;
618             line-height: 1.3;
619             height: 1.3em;
620             }
621              
622             ul.frames .name {
623             margin-right: 10px;
624             }
625             }
626              
627             /* ---------------------------------------------------------------------
628             * Monospace
629             * --------------------------------------------------------------------- */
630              
631             pre, code, .repl input, .repl .prompt span, textarea {
632             font-family: menlo, lucida console, monospace;
633             font-size: 8pt;
634             }
635              
636             /* ---------------------------------------------------------------------
637             * Display area
638             * --------------------------------------------------------------------- */
639              
640             .trace_info {
641             background: #fff;
642             padding: 6px;
643             border-radius: 3px;
644             margin-bottom: 2px;
645             box-shadow: 0 0 10px rgba(0, 0, 0, 0.03), 1px 1px 0 rgba(0, 0, 0, 0.05), -1px 1px 0 rgba(0, 0, 0, 0.05), 0 0 0 4px rgba(0, 0, 0, 0.04);
646             }
647              
648             /* Titlebar */
649             .trace_info .title {
650             background: #f1f1f1;
651              
652             box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
653             overflow: hidden;
654             padding: 6px 10px;
655              
656             border: solid 1px #ccc;
657             border-bottom: 0;
658              
659             border-top-left-radius: 2px;
660             border-top-right-radius: 2px;
661             }
662              
663             .trace_info .title .name,
664             .trace_info .title .location {
665             font-size: 9pt;
666             line-height: 26px;
667             height: 26px;
668             overflow: hidden;
669             }
670              
671             .trace_info .title .location {
672             float: left;
673             font-weight: bold;
674             font-size: 10pt;
675             }
676              
677             .trace_info .title .location a {
678             color:inherit;
679             text-decoration:none;
680             border-bottom:1px solid #aaaaaa;
681             }
682              
683             .trace_info .title .location a:hover {
684             border-color:#666666;
685             }
686              
687             .trace_info .title .name {
688             float: right;
689             font-weight: 200;
690             }
691              
692             .code, .console, .unavailable {
693             background: #fff;
694             padding: 5px;
695              
696             box-shadow: inset 3px 3px 3px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
697             }
698              
699             .code {
700             margin-bottom: -1px;
701             }
702              
703             .code {
704             padding: 10px 0;
705             overflow: auto;
706             }
707              
708             /* Source unavailable */
709             p.unavailable {
710             padding: 20px 0 40px 0;
711             text-align: center;
712             color: #b99;
713             font-weight: bold;
714             }
715              
716             p.unavailable:before {
717             content: '\00d7';
718             display: block;
719              
720             color: #daa;
721              
722             text-align: center;
723             font-size: 40pt;
724             font-weight: normal;
725             margin-bottom: -10px;
726             }
727              
728             @-webkit-keyframes highlight {
729             0% { background: rgba(220, 30, 30, 0.3); }
730             100% { background: rgba(220, 30, 30, 0.1); }
731             }
732             @-moz-keyframes highlight {
733             0% { background: rgba(220, 30, 30, 0.3); }
734             100% { background: rgba(220, 30, 30, 0.1); }
735             }
736             @keyframes highlight {
737             0% { background: rgba(220, 30, 30, 0.3); }
738             100% { background: rgba(220, 30, 30, 0.1); }
739             }
740              
741             .code .highlight {
742             background: rgba(220, 30, 30, 0.1);
743             -webkit-animation: highlight 400ms linear 1;
744             -moz-animation: highlight 400ms linear 1;
745             animation: highlight 400ms linear 1;
746             }
747              
748             /* REPL shell */
749             .console {
750             padding: 0 1px 10px 1px;
751             border-bottom-left-radius: 2px;
752             border-bottom-right-radius: 2px;
753             }
754              
755             .console pre {
756             padding: 10px 10px 0 10px;
757             max-height: 400px;
758             overflow-x: none;
759             overflow-y: auto;
760             margin-bottom: -3px;
761             word-wrap: break-word;
762             white-space: pre-wrap;
763             }
764              
765             /* .prompt > span + input */
766             .console .prompt {
767             display: table;
768             width: 100%;
769             }
770              
771             .console .prompt span,
772             .console .prompt input {
773             display: table-cell;
774             }
775              
776             .console .prompt span {
777             width: 1%;
778             padding-right: 5px;
779             padding-left: 10px;
780             }
781              
782             .console .prompt input {
783             width: 99%;
784             }
785              
786             /* Input box */
787             .console input,
788             .console input:focus {
789             outline: 0;
790             border: 0;
791             padding: 0;
792             background: transparent;
793             margin: 0;
794             }
795              
796             /* Hint text */
797             .hint {
798             margin: 15px 0 20px 0;
799             font-size: 8pt;
800             color: #8080a0;
801             padding-left: 20px;
802             }
803              
804             .hint:before {
805             content: '\25b2';
806             margin-right: 5px;
807             opacity: 0.5;
808             }
809              
810             /* ---------------------------------------------------------------------
811             * Variable infos
812             * --------------------------------------------------------------------- */
813              
814             .sub {
815             padding: 10px 0;
816             margin: 10px 0;
817             }
818              
819             .sub:before {
820             content: '';
821             display: block;
822             width: 100%;
823             height: 4px;
824              
825             border-radius: 2px;
826             background: rgba(0, 150, 200, 0.05);
827             box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.7), inset 0 0 0 1px rgba(0, 0, 0, 0.04), inset 2px 2px 2px rgba(0, 0, 0, 0.07);
828             }
829              
830             .sub h3 {
831             color: #39a;
832             font-size: 1.1em;
833             margin: 10px 0;
834             text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
835              
836             -webkit-font-smoothing: antialiased;
837             }
838              
839             .sub .inset {
840             overflow-y: auto;
841             }
842              
843             .sub table {
844             table-layout: fixed;
845             }
846              
847             .sub table td {
848             border-top: dotted 1px #ddd;
849             padding: 7px 1px;
850             }
851              
852             .sub table td.name {
853             width: 150px;
854              
855             font-weight: bold;
856             font-size: 0.8em;
857             padding-right: 20px;
858              
859             word-wrap: break-word;
860             }
861              
862             .sub table td pre {
863             max-height: 15em;
864             overflow-y: auto;
865             }
866              
867             .sub table td pre {
868             width: 100%;
869              
870             word-wrap: break-word;
871             white-space: normal;
872             }
873              
874             /* "(object doesn't support inspect)" */
875             .sub .unsupported {
876             font-family: sans-serif;
877             color: #777;
878             }
879              
880             /* ---------------------------------------------------------------------
881             * Scrollbar
882             * --------------------------------------------------------------------- */
883              
884             nav.sidebar::-webkit-scrollbar,
885             .inset pre::-webkit-scrollbar,
886             .console pre::-webkit-scrollbar,
887             .code::-webkit-scrollbar {
888             width: 10px;
889             height: 10px;
890             }
891              
892             .inset pre::-webkit-scrollbar-thumb,
893             .console pre::-webkit-scrollbar-thumb,
894             .code::-webkit-scrollbar-thumb {
895             background: #ccc;
896             border-radius: 5px;
897             }
898              
899             nav.sidebar::-webkit-scrollbar-thumb {
900             background: rgba(0, 0, 0, 0.0);
901             border-radius: 5px;
902             }
903              
904             nav.sidebar:hover::-webkit-scrollbar-thumb {
905             background-color: #999;
906             background: -webkit-linear-gradient(left, #aaa, #999);
907             }
908              
909             .console pre:hover::-webkit-scrollbar-thumb,
910             .inset pre:hover::-webkit-scrollbar-thumb,
911             .code:hover::-webkit-scrollbar-thumb {
912             background: #888;
913             }
914             </style>
915              
916             [%# IE8 compatibility crap %]
917             <script>
918             (function() {
919             var elements = ["section", "nav", "header", "footer", "audio"];
920             for (var i = 0; i < elements.length; i++) {
921             document.createElement(elements[i]);
922             }
923             })();
924             </script>
925              
926             <script>
927             if (window.Turbolinks) {
928             for(var i=0; i < document.styleSheets.length; i++) {
929             if(document.styleSheets[i].href)
930             document.styleSheets[i].disabled = true;
931             }
932             document.addEventListener("page:restore", function restoreCSS(e) {
933             for(var i=0; i < document.styleSheets.length; i++) {
934             document.styleSheets[i].disabled = false;
935             }
936             document.removeEventListener("page:restore", restoreCSS, false);
937             });
938             }
939             </script>
940             </head>
941             <body>
942             <div class="top">
943             <header class="exception">
944             <h2><strong>Exception Occurs</strong>[% IF request.request_uri %] <span>at [% request.request_uri %]</span>[% END %]</h2>
945             <p>[% message %]</p>
946             </header>
947             </div>
948              
949             <section class="backtrace">
950             <nav class="sidebar">
951             <nav class="tabs">
952             <a href="#" id="application_frames">Application Frames</a>
953             <a href="#" id="all_frames">All Frames</a>
954             </nav>
955             <ul class="frames">
956             [% FOREACH frame IN backtrace_frames %]
957             <li class="[% frame.context %]" data-context="[% frame.context %]" data-index="[% loop.index %]">
958             <span class='stroke'></span>
959             <i class="icon [% frame.context %]"></i>
960             <div class="info">
961             <div class="name">
962             [% next_frame = loop.peek_next %]
963             [% IF next_frame AND next_frame.subroutine %]
964             <strong>[% next_frame.module_name %]</strong><span class='method'>[% next_frame.method_name %]</span>
965             [% END %]
966             </div>
967             <div class="location">
968             <span class="filename">[% frame.filename || '(no file)' %]</span>[% IF frame.line %], line <span class="line">[% frame.line %]</span>[% END %]
969             </div>
970             </div>
971             </li>
972             [% END %]
973             </ul>
974             </nav>
975              
976             [% FOREACH frame IN backtrace_frames %]
977             <div class="frame_info" id="frame_info_[% loop.index %]" style="display:none;">
978             [% frame.info_html | raw %]
979             </div>
980             [% END %]
981             </section>
982             </body>
983             <script>
984             (function() {
985             var previousFrame = null;
986             var previousFrameInfo = null;
987             var allFrames = document.querySelectorAll("ul.frames li");
988             var allFrameInfos = document.querySelectorAll(".frame_info");
989              
990             function apiCall(method, opts, cb) {
991             // TODO: implement it
992             return;
993             var OID = '';
994             var req = new XMLHttpRequest();
995             req.open("POST", "/__better_errors/" + OID + "/" + method, true);
996             req.setRequestHeader("Content-Type", "application/json");
997             req.send(JSON.stringify(opts));
998             req.onreadystatechange = function() {
999             if(req.readyState == 4) {
1000             var res = JSON.parse(req.responseText);
1001             cb(res);
1002             }
1003             };
1004             }
1005              
1006             function escapeHTML(html) {
1007             return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
1008             }
1009              
1010             function REPL(index) {
1011             this.index = index;
1012              
1013             this.previousCommands = [];
1014             this.previousCommandOffset = 0;
1015             }
1016              
1017             REPL.all = [];
1018              
1019             REPL.prototype.install = function(containerElement) {
1020             this.container = containerElement;
1021              
1022             this.promptElement = this.container.querySelector(".prompt span");
1023             this.inputElement = this.container.querySelector("input");
1024             this.outputElement = this.container.querySelector("pre");
1025              
1026             this.inputElement.onkeydown = this.onKeyDown.bind(this);
1027              
1028             this.setPrompt(">>");
1029              
1030             REPL.all[this.index] = this;
1031             }
1032              
1033             REPL.prototype.focus = function() {
1034             this.inputElement.focus();
1035             };
1036              
1037             REPL.prototype.setPrompt = function(prompt) {
1038             this._prompt = prompt;
1039             this.promptElement.innerHTML = escapeHTML(prompt);
1040             };
1041              
1042             REPL.prototype.getInput = function() {
1043             return this.inputElement.value;
1044             };
1045              
1046             REPL.prototype.setInput = function(text) {
1047             this.inputElement.value = text;
1048              
1049             if(this.inputElement.setSelectionRange) {
1050             // set cursor to end of input
1051             this.inputElement.setSelectionRange(text.length, text.length);
1052             }
1053             };
1054              
1055             REPL.prototype.writeRawOutput = function(output) {
1056             this.outputElement.innerHTML += output;
1057             this.outputElement.scrollTop = this.outputElement.scrollHeight;
1058             };
1059              
1060             REPL.prototype.writeOutput = function(output) {
1061             this.writeRawOutput(escapeHTML(output));
1062             };
1063              
1064             REPL.prototype.sendInput = function(line) {
1065             var self = this;
1066             apiCall("eval", { "index": this.index, source: line }, function(response) {
1067             if(response.error) {
1068             self.writeOutput(response.error + "\n");
1069             }
1070             self.writeOutput(self._prompt + " ");
1071             self.writeRawOutput(response.highlighted_input + "\n");
1072             self.writeOutput(response.result);
1073             self.setPrompt(response.prompt);
1074             });
1075             };
1076              
1077             REPL.prototype.onEnterKey = function() {
1078             var text = this.getInput();
1079             if(text != "" && text !== undefined) {
1080             this.previousCommandOffset = this.previousCommands.push(text);
1081             }
1082             this.setInput("");
1083             this.sendInput(text);
1084             };
1085              
1086             REPL.prototype.onNavigateHistory = function(direction) {
1087             this.previousCommandOffset += direction;
1088              
1089             if(this.previousCommandOffset < 0) {
1090             this.previousCommandOffset = -1;
1091             this.setInput("");
1092             return;
1093             }
1094              
1095             if(this.previousCommandOffset >= this.previousCommands.length) {
1096             this.previousCommandOffset = this.previousCommands.length;
1097             this.setInput("");
1098             return;
1099             }
1100              
1101             this.setInput(this.previousCommands[this.previousCommandOffset]);
1102             };
1103              
1104             REPL.prototype.onKeyDown = function(ev) {
1105             if(ev.keyCode == 13) {
1106             this.onEnterKey();
1107             } else if(ev.keyCode == 38) {
1108             // the user pressed the up arrow.
1109             this.onNavigateHistory(-1);
1110             return false;
1111             } else if(ev.keyCode == 40) {
1112             // the user pressed the down arrow.
1113             this.onNavigateHistory(1);
1114             return false;
1115             }
1116             };
1117              
1118             function switchTo(el) {
1119             if(previousFrameInfo) previousFrameInfo.style.display = "none";
1120             previousFrameInfo = el;
1121              
1122             el.style.display = "block";
1123              
1124             var replInput = el.querySelector('.console input');
1125             if (replInput) replInput.focus();
1126             }
1127              
1128             function selectFrameInfo(index) {
1129             var el = allFrameInfos[index];
1130             if(el) {
1131             if (el.loaded) {
1132             return switchTo(el);
1133             }
1134              
1135             el.loaded = true;
1136              
1137             /*
1138             var repl = el.querySelector(".repl .console");
1139             if(repl) {
1140             new REPL(index).install(repl);
1141             }
1142             */
1143              
1144             switchTo(el);
1145             }
1146             }
1147              
1148             for(var i = 0; i < allFrames.length; i++) {
1149             (function(i, el) {
1150             var el = allFrames[i];
1151             el.onclick = function() {
1152             if(previousFrame) {
1153             previousFrame.className = "";
1154             }
1155             el.className = "selected";
1156             previousFrame = el;
1157              
1158             selectFrameInfo(el.attributes["data-index"].value);
1159             };
1160             })(i);
1161             }
1162              
1163             // Click the first application frame
1164             (
1165             document.querySelector(".frames li.application") ||
1166             document.querySelector(".frames li")
1167             ).onclick();
1168              
1169             var applicationFramesButton = document.getElementById("application_frames");
1170             var allFramesButton = document.getElementById("all_frames");
1171              
1172             applicationFramesButton.onclick = function() {
1173             allFramesButton.className = "";
1174             applicationFramesButton.className = "selected";
1175             for(var i = 0; i < allFrames.length; i++) {
1176             if(allFrames[i].attributes["data-context"].value == "application") {
1177             allFrames[i].style.display = "block";
1178             } else {
1179             allFrames[i].style.display = "none";
1180             }
1181             }
1182             return false;
1183             };
1184              
1185             allFramesButton.onclick = function() {
1186             applicationFramesButton.className = "";
1187             allFramesButton.className = "selected";
1188             for(var i = 0; i < allFrames.length; i++) {
1189             allFrames[i].style.display = "block";
1190             }
1191             return false;
1192             };
1193              
1194             applicationFramesButton.onclick();
1195             })();
1196             </script>
1197             </html>
1198             EOTMPL
1199              
1200             sub variables_info_html {
1201 2     2 0 123 <<'EOTMPL' }
1202             <header class="trace_info">
1203             <div class="title">
1204             <h2 class="name">[% frame.subroutine %]</h2>
1205             <div class="location"><span class="filename">[% frame.filename %]</span></div>
1206             </div>
1207              
1208             [% html_formatted_code_block | raw %]
1209              
1210             <!--div class="repl">
1211             <div class="console">
1212             <pre></pre>
1213             <div class="prompt"><span>&gt;&gt;</span> <input/></div>
1214             </div>
1215             </div-->
1216             </header>
1217              
1218             <!--div class="hint">
1219             TODO: Live Shell (REPL) is not implemented yet.
1220             </div-->
1221              
1222             <div class="variable_info"></div>
1223              
1224             [% IF frame.lexicals %]
1225             <div class="sub">
1226             <h3>Lexicals</h3>
1227             <div class='inset variables'>
1228             <table class="var_table">
1229             [% FOREACH key IN frame.lexicals.keys() %]
1230             <tr><td class="name">[% key %]</td><td><pre>[% frame.lexicals.$key | dump %]</pre></td></tr>
1231             [% end %]
1232             </table>
1233             </div>
1234             </div>
1235             [% END %]
1236              
1237             [% IF args %]
1238             <div class="sub">
1239             <h3>Args</h3>
1240             <div class='inset variables'>
1241             <table class="var_table">
1242             [% FOREACH arg IN args %]
1243             <tr><td class="name">$_[[% loop.index %]]</td><td><pre>[% arg | dump %]</pre></td></tr>
1244             [% end %]
1245             </table>
1246             </div>
1247             </div>
1248             [% END %]
1249             EOTMPL
1250              
1251             1;
1252             __END__
1253              
1254             =encoding utf-8
1255              
1256             =head1 NAME
1257              
1258             Plack::Middleware::BetterStackTrace - Displays better stack trace when your app dies
1259              
1260             =head1 SYNOPSIS
1261              
1262             enable 'BetterStackTrace',
1263             application_caller_subroutine => 'Amon2::Web::handle_request';
1264              
1265             =head1 DESCRIPTION
1266              
1267             This middleware catches exceptions (run-time errors) happening in your
1268             application and displays nice stack trace screen. The stack trace is
1269             also stored in the environment as a plaintext and HTML under the key
1270             C<plack.stacktrace.text> and C<plack.stacktrace.html> respectively, so
1271             that middleware futher up the stack can reference it.
1272              
1273             You're recommended to use this middleware during the development and
1274             use L<Plack::Middleware::HTTPExceptions> in the deployment mode as a
1275             replacement, so that all the exceptions thrown from your application
1276             still get caught and rendered as a 500 error response, rather than
1277             crashing the web server.
1278              
1279             Catching errors in streaming response is not supported.
1280              
1281             This module is based on L<Plack::Middleware::StackTrace> and Better Errors for Ruby L<https://github.com/charliesome/better_errors>.
1282              
1283             =head1 LICENSE
1284              
1285             Perl
1286              
1287             Copyright (C) Tasuku SUENAGA a.k.a. gunyarakun.
1288              
1289             This library is free software; you can redistribute it and/or modify
1290             it under the same terms as Perl itself.
1291              
1292             HTML/CSS/JavaScript
1293              
1294             Copyright (C) 2012 Charlie Somerville
1295              
1296             MIT License
1297              
1298             =head1 AUTHOR
1299              
1300             Tasuku SUENAGA a.k.a. gunyarakun E<lt>tasuku-s-github@titech.acE<gt>
1301              
1302             =head1 TODO
1303              
1304             - REPL
1305             - JSON response
1306              
1307             =head1 SEE ALSO
1308              
1309             L<Plack::Middleware::StackTrace> L<Devel::StackTrace::AsHTML> L<Plack::Middleware> L<Plack::Middleware::HTTPExceptions>
1310              
1311             =cut