view release on metacpan or search on metacpan
1.503 Tue Sep 17 12:14:15 2024 -0200
- start listening on re-forked child
- try fix test for some older perls on freebsd
1.502 Wed Sep 03 19:10:55 2024 -0200
- don't generate Content-Length header for 204 responses
- acknowledge PERL_USE_UNSAFE_INC=0
1.501 Sat Aug 31 09:10:55 2024 -0200
- http/1.1 keepalive support for chunked(streaming) responses
- max_connection_reqs to control requests per keepalive connection
- tweak some tests for better cpantesters matrix
1.500 Tue Aug 20 18:10:55 2024 -0200
Features:
- native interface: access specific parts of request
- http/1.1 keepalive support
- http/1.1 date header
- defer accept, accept4
Backward incompatibly:
- remove adobe flash policy support
1.410 Sat Dec 5 14:32:22 2020 +0800
Features
- Add unix domain socket support (vividsnow++)
# for example:
SV *poll_write_cb;
SV *ext_guard;
struct feer_req *req;
ssize_t expected_cl;
ssize_t received_cl;
enum feer_respond_state responding;
enum feer_receive_state receiving;
bool is_keepalive;
int reqs;
unsigned int in_callback;
unsigned int is_http11:1;
unsigned int poll_write_cb_is_io_handle:1;
unsigned int auto_cl:1;
ssize_t pipelined;
};
static SV* feersum_env_header(pTHX_ struct feer_req *r, SV* name);
static SV* feersum_env_addr(pTHX_ struct feer_conn *c);
static SV* feersum_env_port(pTHX_ struct feer_conn *c);
static ssize_t feersum_env_content_length(pTHX_ struct feer_conn *c);
static SV* feersum_env_io(pTHX_ struct feer_conn *c);
static void feersum_start_response
(pTHX_ struct feer_conn *c, SV *message, AV *headers, int streaming);
static size_t feersum_write_whole_body (pTHX_ struct feer_conn *c, SV *body);
static void feersum_handle_psgi_response(
pTHX_ struct feer_conn *c, SV *ret, bool can_recurse);
static bool feersum_set_keepalive (pTHX_ struct feer_conn *c, bool is_keepalive);
static int feersum_close_handle(pTHX_ struct feer_conn *c, bool is_writer);
static SV* feersum_conn_guard(pTHX_ struct feer_conn *c, SV *guard);
static void start_read_watcher(struct feer_conn *c);
static void stop_read_watcher(struct feer_conn *c);
static void restart_read_timer(struct feer_conn *c);
static void stop_read_timer(struct feer_conn *c);
static void start_write_watcher(struct feer_conn *c);
static void stop_write_watcher(struct feer_conn *c);
static bool request_cb_is_psgi = 0;
static SV *shutdown_cb_cv = NULL;
static bool shutting_down = 0;
static int active_conns = 0;
static double read_timeout = READ_TIMEOUT;
static unsigned int max_connection_reqs = 0;
static SV *feer_server_name = NULL;
static SV *feer_server_port = NULL;
static bool is_tcp = 1;
static bool is_keepalive = KEEPALIVE_CONNECTION;
static ev_io accept_w;
static ev_prepare ep;
static ev_check ec;
struct ev_idle ei;
static struct rinq *request_ready_rinq = NULL;
static AV *psgi_ver;
static SV *psgi_serv10, *psgi_serv11, *crlf_sv;
SvIV_set(self,conn_fd);
struct feer_conn *c = (struct feer_conn *)SvPVX(self);
Zero(c, 1, struct feer_conn);
c->self = self;
c->fd = conn_fd;
c->sa = sa;
c->responding = RESPOND_NOT_STARTED;
c->receiving = RECEIVE_HEADERS;
c->is_keepalive = 0;
c->reqs = 0;
c->pipelined = 0;
ev_io_init(&c->read_ev_io, try_conn_read, conn_fd, EV_READ);
c->read_ev_io.data = (void *)c;
ev_init(&c->read_ev_timer, conn_read_timeout);
c->read_ev_timer.data = (void *)c;
trace3("made conn fd=%d self=%p, c=%p, cur=%"Sz_uf", len=%"Sz_uf"\n",
default:
goto try_write_cleanup;
}
try_write_paused:
trace3("write PAUSED %d, refcnt=%d, state=%d\n", c->fd, SvREFCNT(c->self), c->responding);
stop_write_watcher(c);
goto try_write_cleanup;
try_write_shutdown:
if (likely(c->is_keepalive)) {
trace3("write SHUTDOWN, but KEEP %d, refcnt=%d, state=%d\n", c->fd, SvREFCNT(c->self), c->responding);
stop_write_watcher(c);
change_responding_state(c, RESPOND_NOT_STARTED);
change_receiving_state(c, RECEIVE_WAIT);
if (likely(c->req)) {
if (c->req->buf) SvREFCNT_dec(c->req->buf);
if (likely(c->req->path)) SvREFCNT_dec(c->req->path);
if (likely(c->req->query)) SvREFCNT_dec(c->req->query);
if (likely(c->req->addr)) SvREFCNT_dec(c->req->addr);
if (likely(c->req->port)) SvREFCNT_dec(c->req->port);
if (unlikely(revents & EV_ERROR && !(revents & EV_READ))) {
trace("EV error on read, fd=%d revents=0x%08x\n", w->fd, revents);
goto try_read_error;
}
if (unlikely(c->receiving == RECEIVE_SHUTDOWN))
goto dont_read_again;
trace("try read %d\n",w->fd);
if (unlikely(!c->rbuf)) { // likely = optimize for keepalive requests
trace("init rbuf for %d\n",w->fd);
c->rbuf = newSV(READ_INIT_FACTOR*READ_BUFSZ + 1);
SvPOK_on(c->rbuf);
}
ssize_t space_free = SvLEN(c->rbuf) - SvCUR(c->rbuf);
if (unlikely(space_free < READ_BUFSZ)) { // unlikely = optimize for small
size_t new_len = SvLEN(c->rbuf) + READ_GROW_FACTOR*READ_BUFSZ;
trace("moar memory %d: %"Sz_uf" to %"Sz_uf"\n",
w->fd, (Sz)SvLEN(c->rbuf), (Sz)new_len);
change_receiving_state(c, RECEIVE_SHUTDOWN);
change_responding_state(c, RESPOND_SHUTDOWN);
stop_read_watcher(c);
stop_read_timer(c);
stop_write_watcher(c);
goto try_read_cleanup;
try_read_bad:
trace("bad request %d\n", w->fd);
respond_with_server_error(c, "Malformed request.\n", 0, 400);
// TODO: when keep-alive, close conn instead of fallthrough here.
// fallthrough:
dont_read_again:
trace("done reading %d\n", w->fd);
change_receiving_state(c, RECEIVE_SHUTDOWN);
stop_read_watcher(c);
stop_read_timer(c);
goto try_read_cleanup;
try_read_again_reset_timer:
trace("(reset read timer) %d\n", w->fd);
if (likely(c->responding == RESPOND_NOT_STARTED) && c->receiving >= RECEIVE_HEADERS) {
const char *msg;
if (c->receiving == RECEIVE_HEADERS) {
msg = "Headers took too long.";
}
else {
msg = "Timeout reading body.";
}
respond_with_server_error(c, msg, 0, 408);
} else {
trace("read timeout in keepalive conn: %d\n", c->fd);
stop_write_watcher(c);
stop_read_watcher(c);
stop_read_timer(c);
safe_close_conn(c, "close at read timeout");
change_responding_state(c, RESPOND_SHUTDOWN);
}
read_timeout_cleanup:
stop_read_watcher(c);
stop_read_timer(c);
int err_code;
const char *err;
struct feer_req *req = c->req;
trace("processing headers %d minor_version=%d\n",c->fd,req->minor_version);
bool body_is_required;
bool next_req_follows = 0;
bool got_content_length = 0;
c->is_http11 = (req->minor_version == 1);
c->is_keepalive = is_keepalive && c->is_http11;
c->reqs++;
change_receiving_state(c, RECEIVE_BODY);
if (likely(str_eq("GET", 3, req->method, req->method_len))) {
// Not supposed to have a body. Additional bytes are either a
// mistake, a websocket negotiation or pipelined requests under
// HTTP/1.1
next_req_follows = 1;
}
else {
err_code = 400;
err = "invalid content-length\n";
goto got_bad_request;
}
}
else if (
unlikely(str_case_eq("connection", 10, hdr->name, hdr->name_len)))
{
if (likely(c->is_http11)
&& likely(c->is_keepalive)
&& likely(str_case_eq("close", 5, hdr->value, hdr->value_len)))
{
c->is_keepalive = 0;
trace("setting conn %d to close after response\n", c->fd);
}
else if (
likely(!c->is_http11)
&& likely(is_keepalive)
&& str_case_eq("keep-alive", 10, hdr->value, hdr->value_len))
{
c->is_keepalive = 1;
trace("setting conn %d to keep after response\n", c->fd);
}
}
// TODO: support "Transfer-Encoding: chunked" bodies
}
if (max_connection_reqs > 0 && c->reqs >= max_connection_reqs) {
c->is_keepalive = 0;
trace("reached max requests per connection (%d), will close after response\n", max_connection_reqs);
}
if (likely(next_req_follows)) goto got_it_all; // optimize for GET
else if (likely(got_content_length)) goto got_cl;
if (body_is_required) {
// Go the nginx route...
err_code = 411;
err = "Content-Length required\n";
c->is_http11 ? 1 : 0,
err_code, http_code_to_msg(err_code),
(Ssz)msg_len,
(int)msg_len, msg);
add_sv_to_wbuf(c, sv_2mortal(tmp));
stop_read_watcher(c);
stop_read_timer(c);
change_responding_state(c, RESPOND_SHUTDOWN);
change_receiving_state(c, RECEIVE_SHUTDOWN);
if (c->is_keepalive) c->is_keepalive = 0;
conn_write_ready(c);
}
INLINE_UNLESS_DEBUG bool
str_eq(const char *a, int a_len, const char *b, int b_len)
{
if (a_len != b_len) return 0;
if (a == b) return 1;
int i;
for (i=0; i<a_len && i<b_len; i++) {
add_const_to_wbuf(c, ": ", 2);
add_sv_to_wbuf(c, *val);
add_crlf_to_wbuf(c);
}
if (likely(c->is_http11)) {
#ifdef DATE_HEADER
generate_date_header();
add_const_to_wbuf(c, DATE_BUF, DATE_HEADER_LENGTH);
#endif
if (unlikely(!c->is_keepalive))
add_const_to_wbuf(c, "Connection: close" CRLF, 19);
} else if (unlikely(c->is_keepalive) && !streaming)
add_const_to_wbuf(c, "Connection: keep-alive" CRLF, 24);
if (streaming) {
if (c->is_http11)
add_const_to_wbuf(c, "Transfer-Encoding: chunked" CRLFx2, 30);
else {
add_crlf_to_wbuf(c);
// cant do keep-alive for streaming http/1.0 since client completes read on close
if (unlikely(c->is_keepalive)) c->is_keepalive = 0;
}
}
conn_write_ready(c);
}
static size_t
feersum_write_whole_body (pTHX_ struct feer_conn *c, SV *body)
{
size_t RETVAL;
if (unlikely(SvTRUE(ERRSV))) {
call_died(aTHX_ c, "psgix.io magic");
}
else {
SV *io_glob = SvRV(sv);
GvSV(io_glob) = newRV_inc(c->self);
// Put whatever remainder data into the socket buffer.
// Optimizes for the websocket case.
//
// TODO: For keepalive support the opposite operation is required;
// pull the data out of the socket buffer and back into feersum.
if (likely(c->rbuf && SvOK(c->rbuf) && SvCUR(c->rbuf))) {
STRLEN rbuf_len;
const char *rbuf_ptr = SvPV(c->rbuf, rbuf_len);
IO *io = GvIOp(io_glob);
assert(io != NULL);
PerlIO_unread(IoIFP(io), (const void *)rbuf_ptr, rbuf_len);
sv_setpvs(c->rbuf, "");
}
}
trace("set timeout %f\n", new_read_timeout);
read_timeout = new_read_timeout;
}
RETVAL = read_timeout;
}
OUTPUT:
RETVAL
void
set_keepalive (SV *self, SV *set)
PPCODE:
{
trace("set keepalive %d\n", SvTRUE(set));
is_keepalive = SvTRUE(set);
}
unsigned int
max_connection_reqs (SV *self, ...)
PROTOTYPE: $;$
PREINIT:
unsigned int new_max_connection_reqs = 0;
CODE:
{
if (items > 1) {
RETVAL
int
fileno (struct feer_conn *c)
CODE:
RETVAL = c->fd;
OUTPUT:
RETVAL
bool
is_keepalive (struct feer_conn *c)
CODE:
RETVAL = c->is_keepalive;
OUTPUT:
RETVAL
SV*
response_guard (struct feer_conn *c, ...)
PROTOTYPE: $;$
CODE:
RETVAL = feersum_conn_guard(aTHX_ c, (items == 2) ? ST(1) : NULL);
OUTPUT:
RETVAL
t/51-psgi-streaming.t
t/52-psgi-iohandle.t
t/53-psgi-overloaded.t
t/54-psgix-io.t
t/55-psgi-leak.t
t/60-plack.t
t/61-plack-suite.t
t/62-plack-runner.t
t/63-plack-apps.t
t/64-unixsock.t
t/65-keepalive.t
t/66-pipelining.t
t/99-critic.t
t/99-fixme.t
t/99-manifest.t
t/99-pod-coverage.t
t/99-pod.t
t/Utils.pm
typemap
xt/50-psgi-simple-stress.t
t/perlcriticrc
eg/chat.feersum view on Meta::CPAN
</html>
EOHTML
}
sub start_stream {
my ($r, $client) = @_;
my $w = $r->start_streaming(200, \@html_hdrs);
$handles[$client] = $w;
weaken $w;
$timers[$client] = EV::timer 1,1,sub {
$w->write('<!-- keep-alive -->') if $w;
};
$w->write(<<EOH);
<html>
<head></head>
<body>
<p>Hello! (connection $client)</p>
EOH
}
sub broadcast {
lib/Feersum.pm view on Meta::CPAN
complete header (and also, until Feersum supports otherwise, time-outs while
waiting for a request entity body).
Any exceptions thrown in the handler will generate a warning and not
propagated.
=item C<< set_server_name_and_port($host,$port) >>
Override Feersum's notion of what SERVER_HOST and SERVER_PORT should be.
=item C<< set_keepalive($bool) >>
Override Feersum's default keepalive behavior.
=back
=cut
=head1 GRITTY DETAILS
=head2 Compile Time Options
There are a number of constants at the top of Feersum.xs. If you change any
lib/Feersum.pm view on Meta::CPAN
=item AUTOCORK_WRITES
Controls how response data is written to sockets. If enabled (the default)
the event loop is used to wait until the socket is writable, otherwise a write
is performed immediately. In either case, non-blocking writes are used.
Using the event loop is "nicer" but perhaps introduces latency, hence this
option.
=item KEEPALIVE_CONNECTION
Controls support of keepalive connections. Default is false.
If enabled or set via Feersum->set_keepalive(1), then
"Connection: keep-alive" for HTTP/1.0 and "Connection: close" for HTTP/1.1
are acknowledged.
=item READ_TIMEOUT
Controls read timeout. Default is 5.0 sec. It is also an keepalive timeout.
=item FEERSUM_IOMATRIX_SIZE
Controls the size of the main write-buffer structure in Feersum. Making this
value lower will use slightly less memory per connection at the cost of speed
(and vice-versa for raising the value). The effect is most noticeable when
you're app is making a lot of sparce writes. The default of 64 generally
keeps usage under 4k per connection on full 64-bit platforms when you take
into account the other connection and request structures.
lib/Feersum/Runner.pm view on Meta::CPAN
Blocking => 0,
);
croak "couldn't bind to socket: $!" unless $sock;
}
$self->{sock} = $sock;
my $f = Feersum->endjinn;
$f->use_socket($sock);
if (my $opts = $self->{options}) {
$self->{$_} = delete $opts->{$_} for grep defined($opts->{$_}),
qw/pre_fork keepalive read_timeout max_connection_reqs/;
}
$f->set_keepalive($_) for grep defined, delete $self->{keepalive};
$f->read_timeout($_) for grep $_, delete $self->{read_timeout};
$f->max_connection_reqs($_) for grep $_, delete $self->{max_connection_reqs};
$self->{endjinn} = $f;
return;
}
# for overriding:
sub assign_request_handler { ## no critic (RequireArgUnpacking)
return $_[0]->{endjinn}->request_handler($_[1]);
lib/Feersum/Runner.pm view on Meta::CPAN
Listen on this TCP socket (C<host:port> format).
=item pre_fork
Fork this many worker processes.
The fork is run immediately at startup and after the app is loaded (i.e. in
the C<run()> method).
=item keepalive
Enable/disable http keepalive requests.
=item read_timeout
Set read/keepalive timeout in seconds.
=item max_connection_reqs
Set max requests per connection in case of keepalive - 0(default) for unlimited.
=item quiet
Don't be so noisy. (default: on)
=item app_file
Load this filename as a native feersum app.
=back
picohttpparser-git/bench.c view on Meta::CPAN
#define REQ \
"GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n" \
"Host: www.kittyhell.com\r\n" \
"User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 " \
"Pathtraq/0.9\r\n" \
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" \
"Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n" \
"Accept-Encoding: gzip,deflate\r\n" \
"Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n" \
"Keep-Alive: 115\r\n" \
"Connection: keep-alive\r\n" \
"Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; " \
"__utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; " \
"__utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n" \
"\r\n"
int main(void)
{
const char *method;
size_t method_len;
const char *path;
t/05-streaming.t view on Meta::CPAN
};
});
is exception {
$evh->use_socket($socket);
}, undef, 'assigned socket';
sub client {
my $cnum = sprintf("%04d",shift);
my $is_chunked = shift || 0;
my $is_keepalive = shift || 0;
$cv->begin;
my $h; $h = simple_client GET => '/foo',
name => $cnum,
timeout => 15,
proto => $is_chunked ? '1.1' : '1.0',
headers => {
"Accept" => "*/*",
'X-Client' => $cnum,
},
$is_chunked && $is_keepalive ? (keepalive => 1) : (),
sub {
my ($body, $headers) = @_;
is $headers->{Status}, 200, "$cnum got 200"
or diag $headers->{Reason};
if ($is_chunked) {
is $headers->{HTTPVersion}, '1.1';
is $headers->{'transfer-encoding'}, "chunked", "$cnum got chunked!";
if (!$is_keepalive) {
is $headers->{'connection'}, 'close', "$cnum conn close";
} else {
ok !exists($headers->{'connection'}), 'conn keep';
}
}
else {
is $headers->{HTTPVersion}, '1.0';
ok !exists $headers->{'transfer-encoding'}, "$cnum not chunked!";
if ($is_keepalive) {
is $headers->{'connection'}, 'keep-alive', "$cnum conn keep";
} else {
ok !exists($headers->{'connection'}), 'conn close';
}
}
is_deeply [split /\n/,$body], [
"$cnum Hello streaming world! chunk one",
"$cnum Hello streaming world! chunk 'two'",
"$cnum Hello streaming world! chunk three",
], "$cnum got all three lines"
or do {
t/05-streaming.t view on Meta::CPAN
};
$cv->end;
undef $h;
};
}
client(1000+$_,1) for (1..CLIENTS_11);
client(2000+$_,0) for (1..CLIENTS_10); # HTTP/1.0 style
$evh->set_keepalive(1);
client(1000+$_,1,1) for (1..CLIENTS_11);
$cv->recv;
is $started, CLIENTS, 'handlers started';
is $finished, CLIENTS, 'handlers finished';
pass "all done";
t/07-graceful-shutdown.t view on Meta::CPAN
$cv->begin;
my $w = $r->start_streaming("200 OK", ['Content-Type' => 'text/plain']);
$started++;
isa_ok($w, 'Feersum::Connection::Writer', "got a writer $cnum");
isa_ok($w, 'Feersum::Connection::Handle', "... it's a handle $cnum");
my $t; $t = AE::timer 1.5+rand(0.5), 0, sub {
is exception {
$w->write("So graceful!\n");
$w->close();
}, undef, "wrote after waiting a little $cnum";
undef $t; # keep timer alive until it runs
undef $w;
$cv->end;
$finished++;
};
});
is exception {
$evh->use_socket($socket);
}, undef, 'assigned socket';
t/51-psgi-streaming.t view on Meta::CPAN
}
using_writer_and_1_0: {
my $cv = AE::cv;
$cv->begin;
my $h2; $h2 = simple_client GET => '/', proto => '1.0', sub {
my ($body, $headers) = @_;
is $headers->{'Status'}, 200, "Response OK";
is $headers->{'content-type'}, 'application/json', "... is JSON";
ok !$headers->{'transfer-encoding'}, '... was not chunked';
isnt $headers->{'connection'}, 'keep-alive', '... got close';
is $body, q({"message":"O hai 3"}), "... correct body";
$cv->end;
undef $h2;
};
$cv->recv;
}
$evh->set_keepalive(1);
using_writer_and_1_1: {
my $cv = AE::cv;
$cv->begin;
my $h2; $h2 = simple_client GET => '/', proto => '1.1', keepalive => 1, sub {
my ($body, $headers) = @_;
is $headers->{'Status'}, 200, "Response OK";
is $headers->{'content-type'}, 'application/json', "... is JSON";
ok $headers->{'transfer-encoding'}, '... not chunked';
isnt $headers->{'connection'}, 'close', '... keep';
is $body, q({"message":"O hai 4"}), "... correct de-chunked body";
$cv->end;
undef $h2;
};
$cv->recv;
t/63-plack-apps.t view on Meta::CPAN
my $req = HTTP::Request->new(GET =>
"http://localhost/63-plack-apps.t");
my $res = $cb->($req);
my $s = "# IS THIS FILE"." STATICALLY SERVED?";
is $res->code, 200;
like $res->content, qr/^\Q$s\E$/m, "found static line (cascade)";
}
);
via_redirect: test_psgi(
# these two tests fail randomly on some platforms with keep_alive on.
# from the pod...
# BUGS - Keep-alive is ignored completely.
ua => Plack::LWPish->new( no_proxy => [qw/127.0.0.1/], keep_alive => 0 ),
app => builder {
mount '/static' => Plack::App::Cascade->new(apps => [
Plack::App::File->new(root => 'notfound')->to_app,
Plack::App::File->new(root => 't')->to_app,
]);
mount '/' => sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
if ($req->path eq '/') {
t/65-keepalive.t view on Meta::CPAN
plan tests => 34;
pass 'using sock path '.$sock_path;
my $pid = fork();
if ($pid == 0) { # child
eval {
my $runner = Feersum::Runner->new(
listen => [$sock_path],
keepalive => 1,
read_timeout => 1,
max_connection_reqs => 4,
app => sub {
my $r = shift;
pass 'got request http/1.'.($r->is_http11 ? 1 : 0);
$r->send_response(200, [], []);
}
);
ok $runner, "got a runner";
$runner->run;
t/65-keepalive.t view on Meta::CPAN
unlike $_[1], qr(Connection), 'http/1.1 no connection header';
$hdl->push_write("GET / HTTP/1.1\015\012Connection: close\015\012\015\012");
$hdl->push_read(line => "\015\012\015\012" => sub {
like $_[1], qr(Connection: close), 'http/1.1 connection close reply';
$hdl->on_read(sub {});
});
});
$cv->recv;
undef $hdl;
# keep alive timeout
$socket = IO::Socket::UNIX->new(
Peer => $sock_path,
Type => SOCK_STREAM,
) or warn $!;
ok $socket, 'client ok';
ok $socket->blocking(0), 'unblock socket';
$cv = AE::cv;
$cv->begin;
$hdl = AnyEvent::Handle->new(
t/65-keepalive.t view on Meta::CPAN
$hdl->destroy;
$cv->send;
},
on_eof => sub {
pass 'server closed connection';
$hdl->destroy;
$cv->send;
},
timeout => 2
);
$hdl->push_write("GET / HTTP/1.0\015\012Connection: keep-alive\015\012\015\012");
$hdl->push_read(line => "\015\012\015\012" => sub {
like $_[1], qr(Connection: keep-alive), 'http/1.0 connection keepalive reply';
$hdl->push_write("GET / HTTP/1.0\015\012\015\012");
$hdl->push_read(line => "\015\012\015\012" => sub {
unlike $_[1], qr(Connection:), 'http/1.0 no connection header';
$hdl->on_read(sub {});
});
});
$cv->recv;
undef $hdl;
# max_connection_reqs
t/66-pipelining.t view on Meta::CPAN
use lib 't'; use Utils;
BEGIN { use_ok('Feersum') };
my ($listen_socket, $port) = get_listen_socket();
ok $listen_socket, "made listen socket";
ok $listen_socket->fileno, "has a fileno";
my $evh = Feersum->new();
# Enable keep-alive which is needed for all tests
$evh->set_keepalive(1);
# Set a shorter read timeout to fail faster in case of problems
$evh->read_timeout(2.0);
my $request_count = 0;
$evh->request_handler(sub {
my $r = shift;
isa_ok $r, 'Feersum::Connection', 'got an object!';
$request_count++;
my $method = $r->method;
t/66-pipelining.t view on Meta::CPAN
}
# Add small delays to verify ordering for specific requests
my @res = (
200,
['Content-Type' => 'text/plain'],
["Response $request_count: $method $path" . ($body ? " Body: $body" : "")]
);
# Add delay for certain paths to test response ordering
if ($path =~ /\/delay|\/test2|\/keepalive2/) {
my $w; $w = AE::timer 0.1, 0, sub {
undef $w;
$r->send_response(@res);
};
} else {
$r->send_response(@res)
}
});
is exception {
t/66-pipelining.t view on Meta::CPAN
"GET /test1 HTTP/1.1\r\nHost: localhost\r\n\r\n" .
"GET /test2 HTTP/1.1\r\nHost: localhost\r\n\r\n" .
"POST /post1 HTTP/1.1\r\nHost: localhost\r\nContent-Length: " . length($post_body1) . "\r\n\r\n" . $post_body1 .
"POST /post2 HTTP/1.1\r\nHost: localhost\r\nContent-Length: " . length($post_body2) . "\r\n\r\n" . $post_body2 .
"GET /test3 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
);
$cv->recv;
};
# Part 2: Test keepalive requests (sequential requests on same connection)
subtest 'Keepalive Requests' => sub {
plan tests => 14;
my $cv = AE::cv;
$cv->begin;
my @responses;
my $request_index = 0;
my @requests = (
"GET /keepalive1 HTTP/1.1\r\nHost: localhost\r\n\r\n",
"GET /keepalive2 HTTP/1.1\r\nHost: localhost\r\n\r\n",
"POST /keepalive-post HTTP/1.1\r\nHost: localhost\r\nContent-Length: 10\r\n\r\nKeepAlive!",
"GET /keepalive-end HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
);
my $h; $h = AnyEvent::Handle->new(
connect => ['localhost', $port],
timeout => 5,
on_error => sub {
my ($h, $fatal, $msg) = @_;
fail "client error in keepalive test: $msg";
$cv->send;
},
on_eof => sub {
# Done handling all responses
is(scalar(@responses), 4, 'Got expected number of keepalive responses');
my @parts = map { split /\r\n\r\n/ } @responses;
# First keepalive response
like($parts[0], qr/^HTTP\/1\.1 200 OK/, 'First keepalive response has correct status');
like($parts[1], qr/Response \d+: GET \/keepalive1/, 'First keepalive response has correct body');
# Second keepalive response (with delay)
like($parts[2], qr/^HTTP\/1\.1 200 OK/, 'Second keepalive response has correct status');
like($parts[3], qr/Response \d+: GET \/keepalive2/, 'Second keepalive response has correct body');
# Third keepalive response (POST)
like($parts[4], qr/^HTTP\/1\.1 200 OK/, 'Third keepalive response has correct status');
like($parts[5], qr/Response \d+: POST \/keepalive-post Body: KeepAlive!/, 'Third keepalive response has correct POST body');
# Fourth keepalive response (with close)
like($parts[6], qr/^HTTP\/1\.1 200 OK/, 'Fourth keepalive response has correct status');
like($parts[6], qr/Connection: close/, 'Fourth keepalive response has Connection: close');
like($parts[7], qr/Response \d+: GET \/keepalive-end/, 'Fourth keepalive response has correct body');
$cv->end;
$h->destroy;
},
on_read => sub {
my ($handle) = @_;
last unless my $len = length(my $buf = $handle->rbuf);
# Store response
push @responses, $buf;
t/66-pipelining.t view on Meta::CPAN
$handle->push_write($requests[$request_index]);
}
}
);
# Send first request
$h->push_write($requests[0]);
$cv->recv;
};
# Part 3: Test mixed keepalive and pipelined requests with more POST requests
subtest 'Mixed Keepalive and Pipelined Requests' => sub {
plan tests => 20;
my $cv = AE::cv;
$cv->begin;
my @responses;
my $mixed_phase = 0;
my $h; $h = AnyEvent::Handle->new(
connect => ['localhost', $port],
timeout => 5,
on_error => sub {
my $proto = delete $opts{proto} || '1.1';
my $body = delete $opts{body} || '';
$headers->{'User-Agent'} ||= 'FeersumSimpleClient/1.0';
$headers->{'Host'} ||= $host.':'.$port;
if (length($body)) {
$headers->{'Content-Length'} ||= length($body);
$headers->{'Content-Type'} ||= 'text/plain';
}
# HTTP/1.1 default is 'keep-alive'
$headers->{'Connection'} ||= 'close' if $proto eq '1.1' && !$opts{keepalive};
my $head = join($CRLF, map {$_.': '.$headers->{$_}} sort keys %$headers);
my $http_req = "$method $uri HTTP/$proto$CRLF";
$strong_h->push_write($http_req);
$strong_h->push_write($head.$CRLF.$CRLF.$body)
unless $opts{skip_head};
# $http_req =~ s/$CRLF/<CRLF>\n/sg;