File Coverage

blib/lib/Mojo/Content.pm
Criterion Covered Total %
statement 159 159 100.0
branch 104 106 98.1
condition 74 78 94.8
subroutine 39 39 100.0
pod 25 25 100.0
total 401 407 98.5


line stmt bran cond sub pod time code
1             package Mojo::Content;
2 64     64   360 use Mojo::Base 'Mojo::EventEmitter';
  64         111  
  64         338  
3              
4 64     64   326 use Carp qw(croak);
  64         117  
  64         3404  
5 64     64   812 use Compress::Raw::Zlib qw(WANT_GZIP Z_STREAM_END);
  64         5032  
  64         3299  
6 64     64   29069 use Mojo::Headers;
  64         167  
  64         412  
7 64     64   24506 use Mojo::SSE qw(build_event parse_event);
  64         170  
  64         4080  
8 64     64   363 use Scalar::Util qw(looks_like_number);
  64         95  
  64         173706  
9              
10             has [qw(auto_decompress auto_relax relaxed skip_body)];
11             has headers => sub { Mojo::Headers->new };
12             has max_buffer_size => sub { $ENV{MOJO_MAX_BUFFER_SIZE} || 262144 };
13             has max_leftover_size => sub { $ENV{MOJO_MAX_LEFTOVER_SIZE} || 262144 };
14              
15             my $BOUNDARY_RE = qr!multipart.*boundary\s*=\s*(?:"([^"]+)"|([\w'(),.:?\-+/]+))!i;
16              
17 1     1 1 3556 sub body_contains { croak 'Method "body_contains" not implemented by subclass' }
18 1     1 1 851 sub body_size { croak 'Method "body_size" not implemented by subclass' }
19              
20 3126 100 100 3126 1 5559 sub boundary { (shift->headers->content_type // '') =~ $BOUNDARY_RE ? $1 // $2 : undef }
      66        
21              
22             sub charset {
23 1081   100 1081 1 2830 my $type = shift->headers->content_type // '';
24 1081 100       9609 return $type =~ /charset\s*=\s*"?([^"\s;]+)"?/i ? $1 : undef;
25             }
26              
27             sub clone {
28 22     22 1 35 my $self = shift;
29 22 100       53 return undef if $self->is_dynamic;
30 18         51 return $self->new(headers => $self->headers->clone);
31             }
32              
33             sub generate_body_chunk {
34 245     245 1 420 my ($self, $offset) = @_;
35              
36 245 100 100     1102 $self->emit(drain => $offset) unless length($self->{body_buffer} //= '');
37 244 100       778 return delete $self->{body_buffer} if length $self->{body_buffer};
38 92 100       293 return '' if $self->{eof};
39              
40 24         82 my $len = $self->headers->content_length;
41 24 100 100     177 return looks_like_number $len && $len == $offset ? '' : undef;
42             }
43              
44 1     1 1 811 sub get_body_chunk { croak 'Method "get_body_chunk" not implemented by subclass' }
45              
46 2182     2182 1 3835 sub get_header_chunk { substr shift->_headers->{header_buffer}, shift, 131072 }
47              
48 2270     2270 1 5296 sub header_size { length shift->_headers->{header_buffer} }
49              
50 62     62 1 128 sub headers_contain { index(shift->_headers->{header_buffer}, shift) >= 0 }
51              
52 7494     7494 1 11475 sub is_chunked { !!shift->headers->transfer_encoding }
53              
54 1004   100 1004 1 1988 sub is_compressed { lc(shift->headers->content_encoding // '') eq 'gzip' }
55              
56 7504     7504 1 16472 sub is_dynamic { !!$_[0]{dynamic} }
57              
58 5438   100 5438 1 16465 sub is_finished { (shift->{state} // '') eq 'finished' }
59              
60 2858     2858 1 5849 sub is_limit_exceeded { !!shift->{limit} }
61              
62 4576     4576 1 13746 sub is_multipart {undef}
63              
64 21   100 21 1 104 sub is_parsing_body { (shift->{state} // '') eq 'body' }
65              
66 2606   100 2606 1 4199 sub is_sse { (shift->headers->content_type // '') eq 'text/event-stream' }
67              
68 1060     1060 1 3662 sub leftovers { shift->{buffer} }
69              
70             sub parse {
71 3065     3065 1 3960 my $self = shift;
72              
73             # Headers
74 3065         6002 $self->_parse_until_body(@_);
75 3065 100       5957 return $self if $self->{state} eq 'headers';
76              
77             # Chunked content
78 2745   100     8582 $self->{real_size} //= 0;
79 2745 100       5249 if ($self->is_chunked) {
    100          
80 139         376 $self->_parse_chunked;
81 139 100 100     420 $self->{state} = 'finished' if ($self->{chunk_state} // '') eq 'finished';
82             }
83              
84             # SSE
85 22         62 elsif ($self->is_sse) { $self->_parse_sse }
86              
87             # Not chunked, pass through to second buffer
88             else {
89 2584         4015 $self->{real_size} += length $self->{pre_buffer};
90 2584   100     4501 my $limit = $self->is_finished && length($self->{buffer}) > $self->max_leftover_size;
91 2584 100       8507 $self->{buffer} .= $self->{pre_buffer} unless $limit;
92 2584         4451 $self->{pre_buffer} = '';
93             }
94              
95             # No content
96 2745 100       5862 if ($self->skip_body) {
97 123         257 $self->{state} = 'finished';
98 123         551 return $self;
99             }
100              
101             # Relaxed parsing
102 2622         4380 my $headers = $self->headers;
103 2622   100     4831 my $len = $headers->content_length // '';
104 2622 100 100     5089 if ($self->auto_relax && !length $len) {
105 112   100     261 my $connection = lc($headers->connection // '');
106 112 100 100     440 $self->relaxed(1) if $connection eq 'close' || !$connection;
107             }
108              
109             # Chunked or relaxed content
110 2622 100 100     4151 if ($self->is_chunked || $self->relaxed) {
111 288   100     934 $self->_decompress($self->{buffer} //= '');
112 288         569 $self->{size} += length $self->{buffer};
113 288         400 $self->{buffer} = '';
114 288         966 return $self;
115             }
116              
117             # Normal content
118 2334 100       8372 $len = 0 unless looks_like_number $len;
119 2334 100 100     9805 if ((my $need = $len - ($self->{size} ||= 0)) > 0) {
120 1259         1944 my $len = length $self->{buffer};
121 1259 100       5416 my $chunk = substr $self->{buffer}, 0, $need > $len ? $len : $need, '';
122 1259         3421 $self->_decompress($chunk);
123 1259         2294 $self->{size} += length $chunk;
124             }
125 2334 100       5035 $self->{state} = 'finished' if $len <= $self->progress;
126              
127 2334         7793 return $self;
128             }
129              
130             sub parse_body {
131 57     57 1 65 my $self = shift;
132 57         111 $self->{state} = 'body';
133 57         139 return $self->parse(@_);
134             }
135              
136             sub progress {
137 2376     2376 1 2777 my $self = shift;
138 2376 100       4630 return 0 unless my $state = $self->{state};
139 2369 100 100     4772 return 0 unless $state eq 'body' || $state eq 'finished';
140 2363   100     7280 return $self->{raw_size} - ($self->{header_size} || 0);
141             }
142              
143             sub write {
144 152     152 1 321 my ($self, $chunk, $cb) = @_;
145              
146 152         245 $self->{dynamic} = 1;
147 152 100       385 $self->{body_buffer} .= $chunk if defined $chunk;
148 152 100       282 $self->once(drain => $cb) if $cb;
149 152 100 100     542 $self->{eof} = 1 if defined $chunk && !length $chunk;
150              
151 152         316 return $self;
152             }
153              
154             sub write_chunk {
155 110     110 1 337 my ($self, $chunk, $cb) = @_;
156              
157 110 100       298 $self->headers->transfer_encoding('chunked') unless $self->{chunked};
158 110         164 @{$self}{qw(chunked dynamic)} = (1, 1);
  110         210  
159              
160 110 100       475 $self->{body_buffer} .= $self->_build_chunk($chunk) if defined $chunk;
161 110 100       429 $self->once(drain => $cb) if $cb;
162 110 100 100     408 $self->{eof} = 1 if defined $chunk && !length $chunk;
163              
164 110         217 return $self;
165             }
166              
167             sub write_sse {
168 57     57 1 78 my ($self, $event, $cb) = @_;
169              
170 57 100       144 $self->headers->content_type('text/event-stream') unless $self->{sse};
171 57         88 $self->{sse} = 1;
172              
173 57 100       103 return $self->write unless defined $event;
174 55         158 return $self->write(build_event($event), $cb);
175             }
176              
177             sub _build_chunk {
178 109     109   217 my ($self, $chunk) = @_;
179              
180             # End
181 109 100       244 return "\x0d\x0a0\x0d\x0a\x0d\x0a" unless length $chunk;
182              
183             # First chunk has no leading CRLF
184 87 100       213 my $crlf = $self->{chunks}++ ? "\x0d\x0a" : '';
185 87         457 return $crlf . sprintf('%x', length $chunk) . "\x0d\x0a$chunk";
186             }
187              
188             sub _decompress {
189 1547     1547   2709 my ($self, $chunk) = @_;
190              
191             # No compression
192 1547 100 100     3029 return $self->emit(read => $chunk) unless $self->auto_decompress && $self->is_compressed;
193              
194             # Decompress
195 62         232 $self->{post_buffer} .= $chunk;
196 62   66     553 my $gz = $self->{gz} //= Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP);
197 62         31716 my $status = $gz->inflate(\$self->{post_buffer}, my $out);
198 62 50       370 $self->emit(read => $out) if defined $out;
199              
200             # Replace Content-Encoding with Content-Length
201 62 100       272 $self->headers->content_length($gz->total_out)->remove('Content-Encoding') if $status == Z_STREAM_END;
202              
203             # Check buffer size
204 62 100 50     341 @$self{qw(state limit)} = ('finished', 1) if length($self->{post_buffer} // '') > $self->max_buffer_size;
205             }
206              
207             sub _headers {
208 4514     4514   4795 my $self = shift;
209 4514 100       11974 return $self if defined $self->{header_buffer};
210 2132         3536 my $headers = $self->headers->to_string;
211 2132 100       7392 $self->{header_buffer} = $headers ? "$headers\x0d\x0a\x0d\x0a" : "\x0d\x0a";
212 2132         8541 return $self;
213             }
214              
215             sub _parse_chunked {
216 139     139   174 my $self = shift;
217              
218             # Trailing headers
219 139 100 100     487 return $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
220              
221 137         331 while (my $len = length $self->{pre_buffer}) {
222              
223             # Start new chunk (ignore the chunk extension)
224 261 100       488 unless ($self->{chunk_len}) {
225 156 100       802 last unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a//;
226 136 100       540 next if $self->{chunk_len} = hex $1;
227              
228             # Last chunk
229 31         64 $self->{chunk_state} = 'trailing_headers';
230 31         54 last;
231             }
232              
233             # Remove as much as possible from payload
234 105 100       248 $len = $self->{chunk_len} if $self->{chunk_len} < $len;
235 105         292 $self->{buffer} .= substr $self->{pre_buffer}, 0, $len, '';
236 105         136 $self->{real_size} += $len;
237 105         214 $self->{chunk_len} -= $len;
238             }
239              
240             # Trailing headers
241 137 100 100     547 $self->_parse_chunked_trailing_headers if ($self->{chunk_state} // '') eq 'trailing_headers';
242              
243             # Check buffer size
244 137 100 100     483 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
245             }
246              
247             sub _parse_chunked_trailing_headers {
248 33     33   76 my $self = shift;
249              
250 33         122 my $headers = $self->headers->parse(delete $self->{pre_buffer});
251 33 100       115 return unless $headers->is_finished;
252 31         64 $self->{chunk_state} = 'finished';
253              
254             # Take care of leftover and replace Transfer-Encoding with Content-Length
255 31         94 $self->{buffer} .= $headers->leftovers;
256 31         135 $headers->remove('Transfer-Encoding');
257 31 100       80 $headers->content_length($self->{real_size}) unless $headers->content_length;
258             }
259              
260             sub _parse_headers {
261 2908     2908   3500 my $self = shift;
262              
263 2908         5319 my $headers = $self->headers->parse(delete $self->{pre_buffer});
264 2908 100       7217 return unless $headers->is_finished;
265 2268         3826 $self->{state} = 'body';
266              
267             # Take care of leftovers
268 2268         4441 my $leftovers = $self->{pre_buffer} = $headers->leftovers;
269 2268         5782 $self->{header_size} = $self->{raw_size} - length $leftovers;
270             }
271              
272             sub _parse_sse {
273 22     22   40 my $self = shift;
274              
275             # Connection established
276 22 100       73 $self->emit('sse') unless $self->{sse};
277 22         63 $self->{sse} = 1;
278              
279             # Parse SSE
280 22         74 while (my $event = parse_event(\$self->{pre_buffer})) { $self->emit(sse => $event) }
  53         128  
281              
282             # Check buffer size
283 22 50 50     97 @$self{qw(state limit)} = ('finished', 1) if length($self->{pre_buffer} // '') > $self->max_buffer_size;
284             }
285              
286             sub _parse_until_body {
287 5900     5900   8921 my ($self, $chunk) = @_;
288              
289 5900   100     14627 $self->{raw_size} += length($chunk //= '');
290 5900         13111 $self->{pre_buffer} .= $chunk;
291 5900 100 100     19099 $self->_parse_headers if ($self->{state} ||= 'headers') eq 'headers';
292 5900 100 100     22182 $self->emit('body') if $self->{state} ne 'headers' && !$self->{body}++;
293             }
294              
295             1;
296              
297             =encoding utf8
298              
299             =head1 NAME
300              
301             Mojo::Content - HTTP content base class
302              
303             =head1 SYNOPSIS
304              
305             package Mojo::Content::MyContent;
306             use Mojo::Base 'Mojo::Content';
307              
308             sub body_contains {...}
309             sub body_size {...}
310             sub get_body_chunk {...}
311              
312             =head1 DESCRIPTION
313              
314             L is an abstract base class for HTTP content containers, based on L
315             7230|https://tools.ietf.org/html/rfc7230> and L, like
316             L and L.
317              
318             =head1 EVENTS
319              
320             L inherits all events from L and can emit the following new ones.
321              
322             =head2 body
323              
324             $content->on(body => sub ($content) {...});
325              
326             Emitted once all headers have been parsed and the body starts.
327              
328             $content->on(body => sub ($content) {
329             $content->auto_upgrade(0) if $content->headers->header('X-No-MultiPart');
330             });
331              
332             =head2 drain
333              
334             $content->on(drain => sub ($content, $offset) {...});
335              
336             Emitted once all data has been written.
337              
338             $content->on(drain => sub ($content) {
339             $content->write_chunk(time);
340             });
341              
342             =head2 read
343              
344             $content->on(read => sub ($content, $bytes) {...});
345              
346             Emitted when a new chunk of content arrives.
347              
348             $content->on(read => sub ($content, $bytes) {
349             say "Streaming: $bytes";
350             });
351              
352             =head2 sse
353              
354             $content->on(sse => sub ($content, $event) {...});
355              
356             Emitted when a new Server-Sent Event (SSE) connection has been established and for each new event that arrives. Note
357             that this event is B and may change without warning!
358              
359             $content->on(sse => sub ($content, $event) {
360             if ($event) { say "Type: $event->{type}, Data: $event->{data}" }
361             else { say "SSE connection established" }
362             });
363              
364             =head1 ATTRIBUTES
365              
366             L implements the following attributes.
367              
368             =head2 auto_decompress
369              
370             my $bool = $content->auto_decompress;
371             $content = $content->auto_decompress($bool);
372              
373             Decompress content automatically if L is true.
374              
375             =head2 auto_relax
376              
377             my $bool = $content->auto_relax;
378             $content = $content->auto_relax($bool);
379              
380             Try to detect when relaxed parsing is necessary.
381              
382             =head2 headers
383              
384             my $headers = $content->headers;
385             $content = $content->headers(Mojo::Headers->new);
386              
387             Content headers, defaults to a L object.
388              
389             =head2 max_buffer_size
390              
391             my $size = $content->max_buffer_size;
392             $content = $content->max_buffer_size(1024);
393              
394             Maximum size in bytes of buffer for content parser, defaults to the value of the C environment
395             variable or C<262144> (256KiB).
396              
397             =head2 max_leftover_size
398              
399             my $size = $content->max_leftover_size;
400             $content = $content->max_leftover_size(1024);
401              
402             Maximum size in bytes of buffer for pipelined HTTP requests, defaults to the value of the C
403             environment variable or C<262144> (256KiB).
404              
405             =head2 relaxed
406              
407             my $bool = $content->relaxed;
408             $content = $content->relaxed($bool);
409              
410             Activate relaxed parsing for responses that are terminated with a connection close.
411              
412             =head2 skip_body
413              
414             my $bool = $content->skip_body;
415             $content = $content->skip_body($bool);
416              
417             Skip body parsing and finish after headers.
418              
419             =head1 METHODS
420              
421             L inherits all methods from L and implements the following new ones.
422              
423             =head2 body_contains
424              
425             my $bool = $content->body_contains('foo bar baz');
426              
427             Check if content contains a specific string. Meant to be overloaded in a subclass.
428              
429             =head2 body_size
430              
431             my $size = $content->body_size;
432              
433             Content size in bytes. Meant to be overloaded in a subclass.
434              
435             =head2 boundary
436              
437             my $boundary = $content->boundary;
438              
439             Extract multipart boundary from C header.
440              
441             =head2 charset
442              
443             my $charset = $content->charset;
444              
445             Extract charset from C header.
446              
447             =head2 clone
448              
449             my $clone = $content->clone;
450              
451             Return a new L object cloned from this content if possible, otherwise return C.
452              
453             =head2 generate_body_chunk
454              
455             my $bytes = $content->generate_body_chunk(0);
456              
457             Generate dynamic content.
458              
459             =head2 get_body_chunk
460              
461             my $bytes = $content->get_body_chunk(0);
462              
463             Get a chunk of content starting from a specific position. Meant to be overloaded in a subclass.
464              
465             =head2 get_header_chunk
466              
467             my $bytes = $content->get_header_chunk(13);
468              
469             Get a chunk of the headers starting from a specific position. Note that this method finalizes the content.
470              
471             =head2 header_size
472              
473             my $size = $content->header_size;
474              
475             Size of headers in bytes. Note that this method finalizes the content.
476              
477             =head2 headers_contain
478              
479             my $bool = $content->headers_contain('foo bar baz');
480              
481             Check if headers contain a specific string. Note that this method finalizes the content.
482              
483             =head2 is_chunked
484              
485             my $bool = $content->is_chunked;
486              
487             Check if C header indicates chunked transfer encoding.
488              
489             =head2 is_compressed
490              
491             my $bool = $content->is_compressed;
492              
493             Check C header for C value.
494              
495             =head2 is_dynamic
496              
497             my $bool = $content->is_dynamic;
498              
499             Check if content will be dynamically generated, which prevents L from working.
500              
501             =head2 is_finished
502              
503             my $bool = $content->is_finished;
504              
505             Check if parser is finished.
506              
507             =head2 is_limit_exceeded
508              
509             my $bool = $content->is_limit_exceeded;
510              
511             Check if buffer has exceeded L.
512              
513             =head2 is_multipart
514              
515             my $bool = $content->is_multipart;
516              
517             False, this is not a L object.
518              
519             =head2 is_parsing_body
520              
521             my $bool = $content->is_parsing_body;
522              
523             Check if body parsing started yet.
524              
525             =head2 is_sse
526              
527             my $bool = $content->is_sse;
528              
529             Check if C header indicates Server-Sent Events (SSE). Note that this method is B and may
530             change without warning!
531              
532             =head2 leftovers
533              
534             my $bytes = $content->leftovers;
535              
536             Get leftover data from content parser.
537              
538             =head2 parse
539              
540             $content
541             = $content->parse("Content-Length: 12\x0d\x0a\x0d\x0aHello World!");
542              
543             Parse content chunk.
544              
545             =head2 parse_body
546              
547             $content = $content->parse_body('Hi!');
548              
549             Parse body chunk and skip headers.
550              
551             =head2 progress
552              
553             my $size = $content->progress;
554              
555             Size of content already received from message in bytes.
556              
557             =head2 write
558              
559             $content = $content->write;
560             $content = $content->write('');
561             $content = $content->write($bytes);
562             $content = $content->write($bytes => sub {...});
563              
564             Write dynamic content non-blocking, the optional drain callback will be executed once all data has been written.
565             Calling this method without a chunk of data will finalize the L and allow for dynamic content to be written
566             later. You can write an empty chunk of data at any time to end the stream.
567              
568             # Make sure previous chunk of data has been written before continuing
569             $content->write('He' => sub ($content) {
570             $content->write('llo!' => sub ($content) {
571             $content->write('');
572             });
573             });
574              
575             =head2 write_chunk
576              
577             $content = $content->write_chunk;
578             $content = $content->write_chunk('');
579             $content = $content->write_chunk($bytes);
580             $content = $content->write_chunk($bytes => sub {...});
581              
582             Write dynamic content non-blocking with chunked transfer encoding, the optional drain callback will be executed once
583             all data has been written. Calling this method without a chunk of data will finalize the L and allow for
584             dynamic content to be written later. You can write an empty chunk of data at any time to end the stream.
585              
586             # Make sure previous chunk of data has been written before continuing
587             $content->write_chunk('He' => sub ($content) {
588             $content->write_chunk('llo!' => sub ($content) {
589             $content->write_chunk('');
590             });
591             });
592              
593             =head2 write_sse
594              
595             $content = $content->write_sse;
596             $content = $content->write_sse($event);
597             $content = $content->write_sse($event => sub {...});
598              
599             Write Server-Sent Event (SSE) non-blocking, the optional drain callback will be executed once all data has been
600             written. Calling this method without an event will finalize the response headers and allow for events to be written
601             later. Note that this method is B and may change without warning!
602              
603             =head1 SEE ALSO
604              
605             L, L, L.
606              
607             =cut