File Coverage

blib/lib/CGI/Parse/PSGI/Streaming.pm
Criterion Covered Total %
statement 51 54 94.4
branch 16 20 80.0
condition 6 8 75.0
subroutine 8 8 100.0
pod 1 1 100.0
total 82 91 90.1


line stmt bran cond sub pod time code
1             package CGI::Parse::PSGI::Streaming;
2 3     3   198080 use strict;
  3         25  
  3         89  
3 3     3   17 use warnings;
  3         6  
  3         138  
4             our $VERSION = '1.0.1'; # VERSION
5 3     3   1003 use HTTP::Response;
  3         47127  
  3         89  
6 3     3   1357 use CGI::Parse::PSGI::Streaming::Handle;
  3         11  
  3         92  
7 3     3   20 use SelectSaver;
  3         6  
  3         1608  
8              
9             # ABSTRACT: creates a filehandle that parses CGI output and writes to a PSGI responder
10              
11              
12             sub parse_cgi_output_streaming_fh {
13 5     5 1 88529 my ($responder) = @_;
14              
15             # ugly-ish way to get a ref to a new filehandle
16 5         12 my $output = \do {local *HANDLE};
  5         32  
17              
18             # state for the callback closure
19 5         19 my $headers; # string, accumulated headers
20             my $response; # HTTP::Response object with parsed headers
21 5         0 my $writer; # the writer object returned by the responder
22              
23             ## no critic(ProhibitTies)
24 5         128 tie *{$output},'CGI::Parse::PSGI::Streaming::Handle', sub {
25             # this callback is invoked with whatever bytes were printed to
26             # the filehandle; it will be called with no argument (or an
27             # undef) when the filehandle is closed
28 9     9   276 my ($data) = @_;
29              
30             # reset the default filehandle to the real STDOUT, just in
31             # case: it's nice to make sure all the callbacks are invoked
32             # with the state they expect
33 9         83 my $saver = SelectSaver->new("::STDOUT");
34              
35             # if we're still parsing the headers
36 9 100       284 if (!$response) {
37 6 50       20 if (defined $data) {
38 6         307 $headers .= $data;
39             }
40             else { # closed file before the end of headers
41 0         0 $headers = "HTTP/1.1 500 Internal Server Error\x0d\x0a";
42             }
43              
44             # still more headers to come, return to the CGI
45 6 100       55 return unless $headers =~ /\x0d?\x0a\x0d?\x0a/;
46              
47             # since we may have received the last bytes of the headers
48             # together with the first bytes of the body, we want to
49             # make sure that $headers contains only the headers, and
50             # $data contains only the body (or '')
51 5         391 ($headers,$data) =
52             ($headers =~ m{\A(.+?)\x0d?\x0a\x0d?\x0a(.*)\z}sm);
53              
54             # HTTP::Response wants things formatted like... an HTTP
55             # response. CGI output is slightly different. Let's cheat.
56 5 50       24 unless ( $headers =~ /^HTTP/ ) {
57 5         17 $headers = "HTTP/1.1 200 OK\x0d\x0a" . $headers;
58             }
59              
60 5         52 $response = HTTP::Response->parse($headers);
61              
62             # RFC 3875 6.2.3
63 5 100 100     1334 if ($response->header('Location') && !$response->header('Status')) {
64 1         96 $response->header('Status', 302);
65             }
66             }
67              
68             # this is not a "elsif"! we may have the start of the body
69             # with the same 'print' as the end of the headers, and we want
70             # to stream out that body already
71 8 50       451 if ($response) { # we have parsed the headers
72 8 50 33     31 if ( $response->code == 500 && !defined($data) ) {
73             # filehandle closed after a raw 500, synthesise a body
74 0         0 $responder->([
75             500,
76             [ 'Content-Type' => 'text/html' ],
77             [ $response->error_as_HTML ]
78             ]);
79 0         0 return;
80             }
81             # we haven't sent the headers to the PSGI backend yet
82 8 100       148 if (!$writer) {
83 5   100     15 my $status = $response->header('Status') || 200;
84 5         234 $status =~ s/\s+.*$//; # remove ' OK' in '200 OK'
85             # PSGI doesn't allow having Status header in the response
86 5         33 $response->remove_header('Status');
87              
88             # we send the status and headers, we get a writer
89             # object back
90             $writer = $responder->([
91             $status,
92             +[
93             map {
94 5         194 my $k = $_;
  8         154  
95 8         21 map { ( $k => _cleanup_newline($_) ) }
  8         288  
96             $response->headers->header($_);
97             } $response->headers->header_field_names
98             ],
99             ]);
100             }
101              
102             # ok, now we have a writer object (either just built, or
103             # built during a previous call). Let's send it whatever
104             # body we have
105 8 100       10607 if (defined $data) {
106 7 100       90 $writer->write($data) if length($data);
107             }
108             else {
109 1         14 $writer->close;
110             }
111             }
112 5         10 };
113              
114 5         20 return $output;
115             }
116              
117             sub _cleanup_newline {
118 8     8   16 local $_ = shift;
119 8         17 s/\r?\n//g;
120 8         42 return $_;
121             }
122              
123             1;
124              
125             __END__