File Coverage

blib/lib/CGI/Parse/PSGI/Streaming.pm
Criterion Covered Total %
statement 50 54 92.5
branch 16 20 80.0
condition 6 8 75.0
subroutine 8 8 100.0
pod 1 1 100.0
total 81 91 89.0


line stmt bran cond sub pod time code
1             package CGI::Parse::PSGI::Streaming;
2 2     2   37660 use strict;
  2         3  
  2         47  
3 2     2   7 use warnings;
  2         2  
  2         66  
4             our $VERSION = '1.0.0'; # VERSION
5 2     2   438 use HTTP::Response;
  2         17924  
  2         45  
6 2     2   782 use CGI::Parse::PSGI::Streaming::Handle;
  2         4  
  2         48  
7 2     2   11 use SelectSaver;
  2         8  
  2         804  
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 4     4 1 3578 my ($responder) = @_;
14              
15             # ugly-ish way to get a ref to a new filehandle
16 4         5 my $output = \do {local *HANDLE};
  4         17  
17              
18             # state for the callback closure
19 4         6 my $headers; # string, accumulated headers
20             my $response; # HTTP::Response object with parsed headers
21 0         0 my $writer; # the writer object returned by the responder
22              
23             ## no critic(ProhibitTies)
24 4         56 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 8     8   11 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 8         39 my $saver = SelectSaver->new("::STDOUT");
34              
35             # if we're still parsing the headers
36 8 100       159 if (!$response) {
37 5 50       11 if (defined $data) {
38 5         7 $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 5 100       31 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 4         30 ($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 4 50       26 unless ( $headers =~ /^HTTP/ ) {
57 4         10 $headers = "HTTP/1.1 200 OK\x0d\x0a" . $headers;
58             }
59              
60 4         27 $response = HTTP::Response->parse($headers);
61              
62             # RFC 3875 6.2.3
63 4 100 100     644 if ($response->header('Location') && !$response->header('Status')) {
64 1         52 $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 7 50       210 if ($response) { # we have parsed the headers
72 7 50 33     26 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 7 100       103 if (!$writer) {
83 4   100     8 my $status = $response->header('Status') || 200;
84 4         112 $status =~ s/\s+.*$//; # remove ' OK' in '200 OK'
85             # PSGI doesn't allow having Status header in the response
86 4         19 $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 4         86 my $k = $_;
  7         85  
95 7         12 map { ( $k => _cleanup_newline($_) ) }
  7         131  
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 7 100       8457 if (defined $data) {
106 6 100       47 $writer->write($data) if length($data);
107             }
108             else {
109 1         12 $writer->close;
110             }
111             }
112 4         4 };
113              
114 4         12 return $output;
115             }
116              
117             sub _cleanup_newline {
118 7     7   9 local $_ = shift;
119 7         8 s/\r?\n//g;
120 7         25 return $_;
121             }
122              
123             1;
124              
125             __END__