The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

UniEvent::HTTP - extremely fast sync/async http client and server framework

SYNOPSIS

    # asynchronous request
    UniEvent::HTTP::http_request({
        uri     => "https://example.com",
        timeout => 5,
        response_callback => sub {
            my ($request, $response, $error) = @_;
            if ($error) {
                warn $error;
                return;
            }
            say Dumper($response->headers);
            say $response->body;
        },
    });
    
    # simple API
    UniEvent::HTTP::http_get("http://mysite.com", sub {
        my ($request, $response, $error) = @_;
        say $res->body;
    });
    
    UniEvent::Loop->default->run;
    
    # synchronous request
    my $response = UniEvent::HTTP::http_get("https://example.com");
    my $response = UniEvent::HTTP::http_request_sync({ uri => "https://example.com", timeout => 3});
    
    # more control
    UniEvent::HTTP::http_request({
        uri               => "https://example.com",
        method            => Protocol::HTTP::METHOD_POST,
        headers           => {...},
        cookies           => {...},
        timeout           => 5,
        follow_redirect   => 1,
        tcp_nodelay       => 1,
        redirection_limit => 5,
        ssl_ctx           => $ssl_ctx,
        proxy             => "socks5://myproxy.com:8080",
        tcp_hints         => $hints,
        compress          => Protocol::HTTP::Compression::gzip,
        allow_compression => [Protocol::HTTP::Compression::gzip, Protocol::HTTP::Compression::deflate],
        body              => $body,
        response_callback => sub {...},
        redirect_callback => sub {...},
        partial_callback  => sub {...},
        continue_callback => sub {...},
    });
    
    
    # http server
    my $server = UniEvent::HTTP::Server->new({
        idle_timeout           => 60,
        max_headers_size       => 16384,
        max_body_size          => 1_000_000,
        tcp_nodelay            => 1,
        max_keepalive_requests => 1000,
        locations              => [
            {
                host => "*",
                port => 80,
                reuse_port => 1,
                backlog => 1024,
            },
            {
                host => "*",
                port => 443,
                reuse_port => 1,
                backlog => 1024,
                ssl_ctx => $ssl_ctx,
            },
        ],
    });
    
    $server->request_callback(sub {
        my $request = shift;
        if ($request->uri->path eq "/hello") {
            $request->respond({
                code    => 200,
                headers => {...},
                body    => "Hi",
            });
        } else {
            $request->respond({code => 404});
        }
    });
    
    my $sig; $sig = UE::signal UE::Signal::SIGINT, sub {
        $sig->stop;
        $server->stop;
    };
    
    $server->run;
    UE::Loop->default->run;
    
    
    # run via plack
    plackup -s UniEvent::HTTP::Simple --listen *:5000 -E deployment app.psgi

    # personal pool
    my $pool = UE::HTTP::Pool->new({ max_connections => 10 });
    $pool->request({
        uri => "...",
        response_callback => sub {...},
    });
    
    # personal client
    my $client = UE::HTTP::Client->new;
    $client->request({
        uri => "...",
        response_callback => sub {...},
    });
    
    # user agent for http session, keeping cookies between requests, like browsers
    my $ua = UE::HTTP::UserAgent->new;
    $ua->request({
        uri => "https://mysite.com/authorize?login=1&password=2",
        response_callback => sub {
            ...
            $ua->request({
                uri => "https://mysite.com/get_info",
                ...
            });
        },
    });
    
    my $serialized = $ua->to_string;
    ...
    my $ua = UE::HTTP::UserAgent->new({serialized => $serialized}); # load session data
    $ua->request({
        uri => "https://mysite.com/get_info",
        ...
    });
   

DESCRIPTION

UniEvent::HTTP is a perl port of C++ panda::unievent::http framework. It contains both synchronous and asynchronous http client and asynchronous http server framework.

It is built on top of Protocol::HTTP http protocol implementation and UniEvent event framework. This library is an UniEvent user, so you need to run UniEvent's loop for it to work.

UniEvent::HTTP supports many features, including various request methods, cookies, request forms, chunks, compression, keep-alive, connection pools and so on.

See UniEvent::HTTP::Manager if you need async multi-process http server with process management.

CLIENT

Client http requests are made via http_get(), http_request(), UniEvent::HTTP::Pool, UniEvent::HTTP::Client and UniEvent::HTTP::UserAgent.

The short description is given below, for full reference see corresponding package's docs.

Simple API

The simple interface is to use http_request() function

    http_request($request);
    

Where $request is a UniEvent::HTTP::Request object or hashref which its constructor supports. Callbacks set in request object will be called during request or after it's finished. See UniEvent::HTTP::Request for more info.

The even more simpler interface is http_get() function

    http_get($uri, $callback);
    

which is the same as

    http_request({
        uri               => $uri,
        response_callback => $callback,
        method            => UE::HTTP::Request::METHOD_GET,
    });

Pool

UniEvent::HTTP::Pool represents a pool of http client connections which makes use of http keep-alive feature and restricts the maximum number of running http requests at a time for certain destination.

Requests made via the same pool object share the same keep-alive connections. Any number of simultaneous requests can be made via one pool but not all of them may be executing at the same time (depending on request uris and config, see UniEvent::HTTP::Pool).

    my $pool = UE::HTTP::Pool->new;
    $pool->request({uri => "http://example.com", ...});
    # .. after request is done
    $pool->request({uri => "http://example.com", ...}); # will reuse connection
    

Simple methods like http_request(), http_get() use global per-loop connection pool.

Client

UniEvent::HTTP::Client represents a single http client connection and is the lowest-level API.

    my $client = UE::HTTP::Client->new;
    $client->request({
        uri               => $uri,
        response_callback => $callback,
    });

Only one request can be made via client at a time. To execute next request you must wait till active request finishes.

SERVER

The short description is given below, for full reference see corresponding package's docs.

Server is an object to be run in single process/thread. If you need to make use of all processor cores, you need to create server in each process/thread. See UniEvent::HTTP::Manager.

Server is created via

    my $server = UE::HTTP::Server->new($config);

And then is run by

    $server->run;
    

Method run() doesn't block anything, it just creates and activates various event handles in UniEvent. You must run the appropriate event loop afterwards.

Receiving requests

There are two main methods of receiving http requests.

The first is setting request_callback. It is called when request is fully received in-memory (including request body).

    $server->request_callback(sub {
        my $request = shift;
        say $request->method_str;
        say $request->uri;
        say Dumper($request->headers);
        say $request->body;
        ...
    });

The second is setting route_callback which is called as early as possible but after all headers arrived. It is expected that user will decide how to process the request depending on it's URI and other properties.

    $server->route_callback(sub {
        my $request = shift;
        say $request->method_str;
        say $request->uri;
        say Dumper($request->headers);
        say $request->body; # ! may be empty or partially available !
        
        if ($request->uri->path eq "/path1") {
            # process request when it's fully received in-memory
            $request->receive_callback(sub {
                my $request = shift;
                say $request->body; # now it's fully available in-memory
            });
        }
        elsif ($request->uri->path eq "/put_file") {
            $request->enable_partial;
            $request->partial_callback(sub {
                my ($request, $error) = @_;
                say $request->body; # will grow from call to call
                # write $request->body to disk or do whatever
                $request->body(""); # clear body to avoid in-memory accumulation
                
                if ($request->is_done) {
                    # request is fully received - finish stuff and send response
                }
            });
        }
    });

If uri is "/path1" we process request as in example before, after fully receiving all request.

If uri is "/put_file" we enable partial mode and set partial_callback which will be called one or more times, every time new data arrives from network. When it is called for the last time, $request->is_done() will be true. In this callback request's properties like body() get filled with more and more data. We can use incremental parsing or write data to disk asynchronously or whatsoever. If any error occurs during request receival, callback will be called with $error indicating error occured. In this case no more calls will be made and $request->is_done will not be true.

Sending response

Regardless of the way request is received, a response can be given asynchronously at any time - immediately or some time on the future.

Response can be fully given at once including body, or can be given partially - without body. The latter case is used to respond with chunks.

    # respond immediately
    $server->request_callback(sub {
        my $request = shift;
        $request->respond({
            code => 200,
            body => "hi",
        });
    });

    # respond later
    $server->request_callback(sub {
        my $request = shift;
        
        $someobject->when_some_event_occurs(sub {
            $request->respond({
                code => 200,
                body => "hi",
            });
        });
    });

    # respond with chunks
    $server->request_callback(sub {
        my $request = shift;
        $request->respond({
            code    => 200,
            chunked => 1,
        });
        
        $someobject->on_file_read(sub {
            ...
            $request->response->send_chunk($data);
            
            if ($last) {
                $request->response->send_final_chunk;
            }
        });
    });

FUNCTIONS

http_request($request || \%request_params, [$loop = default])

Starts asynchronous http request in the given event UniEvent::Loop. The same as

    UniEvent::HTTP::Pool::instance($loop)->request($request);

See UniEvent::HTTP::Request and UniEvent::HTTP::Pool for more info.

http_request_sync($request || \%request_params)

Does http request synchronously and returns result as UniEvent::HTTP::Response object.

    my $response = http_request_sync({....});

Any callbacks set in request will be called during request execution as if request was async. However it is not allowed to start another synchronous http request recursively from those callbacks. All callbacks will be called before the function returns.

If an error occurs during request, function may return undef instead of response or incomplete response object if error occured after we started receiving response. If you need to inspect error, use multiple return value context

    my ($response, $error) = http_request_sync({....});
    

In case of error, $error will be an XS::ErrorCode object.

The similar effect can be achieved with

    my $loop = UE::Loop->new;
    
    my ($response, $error);
    http_request({
        response_callback => sub { (undef, $response, $error) = @_; }
    }, $loop);
    
    $loop->run;
    # use $response from here

http_get($uri, [$response_callback], [$loop = default])

Simpler form of http request with method GET.

If $response_callback is given, then it is a shortcut for asynchronous http request

    http_request({
        uri => $uri,
        response_callback => $response_callback,
    }, $loop);

Otherwise, it is a shortcut for synchronous http request

    http_request_sync({uri => $uri});

STORING ARBITRARY DATA IN REQUEST/RESPONSE/ETC OBJECTS

All this library's objects stores all its data in a place inaccessible from perl code. You can't get access to it, neither corrupt it, so that you can freely store arbitrary data in objects as in hashref. By default, for efficiency, objects are created as blessed references to undef, so you need to upgrade it first to hashref via XS::Framework.

    XS::Framework::obj2hv($request);
    $request->{property} = "value";

REFERENCE

UniEvent::HTTP::Request

UniEvent::HTTP::Response

UniEvent::HTTP::Pool

UniEvent::HTTP::Client

UniEvent::HTTP::RedirectContext

UniEvent::HTTP::UserAgent

UniEvent::HTTP::Server

UniEvent::HTTP::ServerRequest

UniEvent::HTTP::ServerResponse

Plack::Handler::UniEvent::HTTP::Simple

AUTHOR

Pronin Oleg <syber@crazypanda.ru>

Ivan Baidakou <dmol@cpan.org>

Grigory Smorkalov <g.smorkalov@crazypanda.ru>

Andrew Selivanov <andrew.selivanov@gmail.com>

Crazy Panda LTD

LICENSE

You may distribute this code under the same terms as Perl itself.