File Coverage

blib/lib/POE/Declare/HTTP/Client.pm
Criterion Covered Total %
statement 40 42 95.2
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 54 56 96.4


line stmt bran cond sub pod time code
1             package POE::Declare::HTTP::Client;
2              
3             =pod
4              
5             =head1 NAME
6              
7             POE::Declare::HTTP::Client - A simple HTTP client based on POE::Declare
8              
9             =head1 SYNOPSIS
10              
11             # Create the web server
12             my $http = POE::Declare::HTTP::Client->new(
13             Timeout => 10,
14             MaxRedirect => 7,
15             ResponseEvent => \&on_response,
16             ShutdownEvent => \&on_shutdown,
17             );
18            
19             # Control with methods
20             $http->start;
21             $http->GET('http://google.com');
22             $http->stop;
23              
24             =head1 DESCRIPTION
25              
26             This module provides a simple HTTP client based on L.
27              
28             The implemenetation is intentionally minimalist, making this module an ideal
29             choice for creating specialised web clients embedded in larger applications.
30              
31             =head1 METHODS
32              
33             =cut
34              
35 1     1   942 use 5.008;
  1         4  
  1         41  
36 1     1   7 use strict;
  1         1  
  1         33  
37 1     1   5 use warnings;
  1         11  
  1         35  
38 1     1   6 use Scalar::Util 1.19 ();
  1         24  
  1         19  
39 1     1   1031 use Params::Util 1.00 ();
  1         3310  
  1         24  
40 1     1   1001 use HTTP::Status ();
  1         5561  
  1         40  
41 1     1   1036 use HTTP::Request 5.827 ();
  1         29796  
  1         30  
42 1     1   1050 use HTTP::Request::Common ();
  1         2263  
  1         27  
43 1     1   5272 use HTTP::Response 5.830 ();
  1         3920  
  1         29  
44 1     1   1084 use POE 1.293 ();
  1         66362  
  1         45  
45 1     1   1379 use POE::Filter::HTTP::Parser 1.06 ();
  1         24991  
  1         27  
46 1     1   1140 use POE::Wheel::ReadWrite ();
  1         142777  
  1         23  
47 1     1   1027 use POE::Wheel::SocketFactory ();
  1         13110  
  1         83  
48              
49             our $VERSION = '0.05';
50              
51             use POE::Declare 0.53 {
52 0           Timeout => 'Param',
53             MaxRedirect => 'Param',
54             ResponseEvent => 'Message',
55             ShutdownEvent => 'Message',
56             request => 'Internal',
57             previous => 'Internal',
58             factory => 'Internal',
59             socket => 'Internal',
60             redirects => 'Internal',
61 1     1   594 };
  0            
62              
63              
64              
65              
66              
67             ######################################################################
68             # Constructor and Accessors
69              
70             =pod
71              
72             =head2 new
73              
74             my $server = POE::Declare::HTTP::Client->new(
75             ResponseEvent => \&on_response,
76             ShutdownEvent => \&on_shutdown,
77             );
78              
79             The C constructor sets up a reusable HTTP client that can be enabled
80             and disabled repeatedly as needed.
81              
82             =cut
83              
84              
85              
86              
87              
88             ######################################################################
89             # Control Methods
90              
91             =pod
92              
93             =head2 start
94              
95             The C method enables the web server. If the server is already running,
96             this method will shortcut and do nothing.
97              
98             If called before L has been started, the web server will start
99             immediately once L is running.
100              
101             =cut
102              
103             sub start {
104             my $self = shift;
105             unless ( $self->spawned ) {
106             $self->spawn;
107             }
108             return 1;
109             }
110              
111             =pod
112              
113             =head2 stop
114              
115             The C method disables the web server. If the server is not running,
116             this method will shortcut and do nothing.
117              
118             =cut
119              
120             sub stop {
121             my $self = shift;
122             if ( $self->spawned ) {
123             $self->post('shutdown');
124             }
125             return 1;
126             }
127              
128             =pod
129              
130             =head2 GET
131              
132             $client->GET('http://www.cpan.org/');
133              
134             The C method fetches a named URL via a HTTP GET request.
135              
136             =cut
137              
138             sub GET {
139             shift->request(
140             HTTP::Request::Common::GET(@_)
141             );
142             }
143              
144             =pod
145              
146             =head2 HEAD
147              
148             $client->HEAD('http://www.cpan.org/');
149              
150             The C method fetches headers for a named URL via a HTTP HEAD request.
151              
152             =cut
153              
154             sub HEAD {
155             shift->request(
156             HTTP::Request::Common::HEAD(@_)
157             );
158             }
159              
160             =pod
161              
162             =head2 POST
163              
164             $client->POST('http://www.cpan.org/');
165              
166             The C method fetches a named URL via a HTTP POST request.
167              
168             =cut
169              
170             sub POST {
171             shift->request(
172             HTTP::Request::Common::POST(@_)
173             );
174             }
175              
176             =pod
177              
178             =head2 PUT
179              
180             $client->PUT(
181             'http://127.0.0.1:12345/upload.txt',
182             Content => 'This is the file content',
183             );
184              
185             The C method uploads content to a named URL via a HTTP PUT request.
186              
187             =cut
188              
189             sub PUT {
190             shift->request(
191             HTTP::Request::Common::PUT(@_)
192             );
193             }
194              
195             =pod
196              
197             =head2 DELETE
198              
199             $client->DELETE('http://www.cpan.org/');
200              
201             The C method deletes a resource at a URL via a HTTP DELETE request.
202              
203             =cut
204              
205             sub DELETE {
206             shift->request(
207             HTTP::Request::Common::DELETE(@_)
208             );
209             }
210              
211             =pod
212              
213             =head2 request
214              
215             $client->request( $HTTP_Request );
216              
217             The C method triggers an arbitrary HTTP request.
218              
219             It takes any L object, and will respond with an L
220             object to the C message handler once it is completed.
221              
222             =cut
223              
224             sub request {
225             my $self = shift;
226             my $request = shift;
227             unless ( Params::Util::_INSTANCE($request, 'HTTP::Request') ) {
228             die "Missing or invalid HTTP::Request object";
229             }
230              
231             # Initialise the redirect count
232             $self->{redirects} = $self->MaxRedirect || 0;
233              
234             # Save the request object
235             if ( $self->{request} ) {
236             die "HTTP Client is already processing a request";
237             } else {
238             $self->{request} = $request;
239             }
240              
241             # Hand off to the event that starts the request
242             $self->post('connect');
243             }
244              
245             =pod
246              
247             =head2 running
248              
249             The boolean C method returns true if the client is both spawned and
250             processing a request, or false if not. Note that it does not distinguish
251             between running and idle, and stopped entirely.
252              
253             =cut
254              
255             sub running {
256             defined $_[0]->{request};
257             }
258              
259              
260              
261              
262              
263             ######################################################################
264             # Event Methods
265              
266             sub connect : Event {
267             my $addr = $_[ARG0];
268             my $request = $_[SELF]->{request} or return;
269             my $uri = $request->uri;
270             my $host = ($uri->can('host') and $uri->host) or return;
271             my $port = ($uri->can('port') and $uri->port) || 80;
272              
273             # Start the request timeout
274             $_[SELF]->timeout_start;
275              
276             # Create the socket factory for the request
277             $_[SELF]->{factory} = POE::Wheel::SocketFactory->new(
278             RemoteAddress => $host,
279             RemotePort => $port,
280             SuccessEvent => 'connect_success',
281             FailureEvent => 'connect_failure',
282             );
283             }
284              
285             sub timeout : Timeout(30) {
286             return unless $_[SELF]->{request};
287              
288             if ( $_[SELF]->{factory} ) {
289             # Timeout during connect
290             $_[SELF]->{factory} = undef;
291             $_[SELF]->call(
292             response => HTTP::Status::HTTP_INTERNAL_SERVER_ERROR
293             );
294              
295             } elsif ( $_[SELF]->{socket} ) {
296             # Timeout during send, processing or response
297             $_[SELF]->{socket} = undef;
298             $_[SELF]->call(
299             response => HTTP::Status::HTTP_INTERNAL_SERVER_ERROR
300             );
301              
302             } else {
303             # Unexpected timeout during active request
304             $_[SELF]->call(
305             response => HTTP::Status::HTTP_INTERNAL_SERVER_ERROR
306             );
307              
308             }
309             }
310              
311             sub connect_failure : Event {
312             $_[SELF]->timeout_stop;
313             $_[SELF]->{factory} = undef;
314             $_[SELF]->post(
315             response => HTTP::Status::HTTP_INTERNAL_SERVER_ERROR
316             );
317             }
318              
319             sub connect_success : Event {
320             $_[SELF]->{factory} = undef;
321             $_[SELF]->{socket} = POE::Wheel::ReadWrite->new(
322             Filter => POE::Filter::HTTP::Parser->new,
323             Handle => $_[ARG0],
324             InputEvent => 'socket_response',
325             ErrorEvent => 'socket_error',
326             );
327             $_[SELF]->{request}->protocol('HTTP/1.0');
328             $_[SELF]->{request}->user_agent("POE::Declare::HTTP::Client/$VERSION");
329             $_[SELF]->{socket}->put( $_[SELF]->{request} );
330             }
331              
332             sub socket_error : Event {
333             return unless $_[SELF]->{request};
334              
335             # If the HTTP filter has a response in it's buffer that does not have
336             # a fixed content length, consider it complete and trigger an event.
337             my $response = HTTP::Status::HTTP_INTERNAL_SERVER_ERROR;
338             if ( $_[SELF]->{socket} ) {
339             my $socket = $_[SELF]->{socket};
340             my $filter = $socket->get_input_filter;
341             my $parser = $filter->{parser};
342             if ( $parser->{no_content_length} ) {
343             $response = Params::Util::_INSTANCE(
344             $filter->{parser}->object, 'HTTP::Response',
345             );
346             }
347             }
348              
349             $_[SELF]->post( socket_response => $response );
350             }
351              
352             sub socket_response : Event {
353             return unless $_[SELF]->{request};
354              
355             $_[SELF]->timeout_stop;
356             $_[SELF]->{socket} = undef;
357             $_[SELF]->post( response => $_[ARG0] );
358             }
359              
360             sub response : Event {
361             # Check or create the response
362             my $response = $_[ARG0];
363             unless ( Params::Util::_INSTANCE($response, 'HTTP::Response') ) {
364             $response = HTTP::Response->new( $_[ARG0], $_[ARG1] );
365             }
366              
367             # Associate the response with the original request
368             $response->request( delete $_[SELF]->{request} );
369             if ( $_[SELF]->{previous} ) {
370             $response->previous( delete $_[SELF]->{previous} );
371             }
372              
373             # Handle redirects
374             if ( $response->is_redirect and $_[SELF]->{redirects}-- ) {
375             # Prepare the redirect
376             my $request = $response->request;
377             my $location = $response->header('Location');
378             my $uri = do {
379             local $URI::ABS_ALLOW_RELATIVE_SCHEME = 1;
380             my $base = $response->base;
381             $HTTP::URI_CLASS->new($location, $base)->abs($base);
382             };
383              
384             # Validate the redirect against the HTTP 1.1 rules
385             if (
386             $request->method =~ /^(?:GET|HEAD)$/
387             and
388             $uri->scheme ne 'file'
389             ) {
390             # Prepare the new request
391             my $referral = $request->clone;
392             $referral->remove_header('Host', 'Cookie');
393             $referral->uri($uri);
394              
395             # Bind the new request
396             $_[SELF]->{request} = $referral;
397             $_[SELF]->{previous} = $response;
398              
399             # Initiate the new request
400             return $_[SELF]->post('connect');
401             }
402             }
403              
404             # Clean up in preparation for the next request
405             delete $_[SELF]->{redirects};
406              
407             # No further actions needed, return normally
408             $_[SELF]->ResponseEvent( $response );
409             }
410              
411             sub shutdown : Event {
412             $_[SELF]->finish;
413             $_[SELF]->ShutdownEvent;
414             }
415              
416              
417              
418              
419              
420             ######################################################################
421             # POE::Declare::Object Methods
422              
423             sub finish {
424             my $self = shift;
425              
426             # Clear out our stuff
427             $self->{request} = undef;
428             $self->{factory} = undef;
429             $self->{socket} = undef;
430              
431             # Clear out the normal POE stuff
432             $self->SUPER::finish(@_);
433             }
434              
435             compile;
436              
437             =pod
438              
439             =head1 SUPPORT
440              
441             Bugs should be always be reported via the CPAN bug tracker at
442              
443             L
444              
445             For other issues, or commercial enhancement or support, contact the author.
446              
447             =head1 AUTHORS
448              
449             Adam Kennedy Eadamk@cpan.orgE
450              
451             =head1 SEE ALSO
452              
453             L, L
454              
455             =head1 COPYRIGHT
456              
457             Copyright 2011 Adam Kennedy.
458              
459             This program is free software; you can redistribute
460             it and/or modify it under the same terms as Perl itself.
461              
462             The full text of the license can be found in the
463             LICENSE file included with this module.
464              
465             =cut