Skip to content

Commit

Permalink
implement connection timeout, do not croak but return an internal 500…
Browse files Browse the repository at this point in the history
… error on connection failures
  • Loading branch information
kazuho authored and tokuhirom committed Nov 22, 2010
1 parent e978f4d commit 29e3c8e
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 47 deletions.
120 changes: 73 additions & 47 deletions lib/Furl/HTTP.pm
Expand Up @@ -9,7 +9,7 @@ use Furl;
use Furl::ConnectionCache;

use Scalar::Util ();
use Errno qw(EAGAIN EINTR EWOULDBLOCK ECONNRESET);
use Errno qw(EAGAIN ECONNRESET EINPROGRESS EINTR EWOULDBLOCK);
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK SEEK_SET SEEK_END);
use Socket qw(
PF_INET SOCK_STREAM
Expand Down Expand Up @@ -225,45 +225,28 @@ sub request {
my $sock = $self->{connection_pool}->steal($host, $port);
my $in_keepalive = defined $sock;
if(!$in_keepalive) {
my ($_host, $_port);
my $err_reason;
if ($proxy) {
(undef, $_host, $_port, undef)
my (undef, $proxy_host, $proxy_port, undef)
= $self->_parse_url($proxy);
}
else {
$_host = $host;
$_port = $port;
}

if ($scheme eq 'http') {
$sock = $self->connect($_host, $_port, $timeout_at);
} else {
$sock = $proxy
? $self->connect_ssl_over_proxy(
$_host, $_port, $host, $port, $timeout_at)
: $self->connect_ssl($_host, $_port, $timeout_at);
}
setsockopt( $sock, IPPROTO_TCP, TCP_NODELAY, 1 )
or Carp::croak("Failed to setsockopt(TCP_NODELAY): $!");
if (WIN32) {
my $tmp = 1;
ioctl( $sock, 0x8004667E, \$tmp )
or Carp::croak("Cannot set flags for the socket: $!");
if ($scheme eq 'http') {
($sock, $err_reason)
= $self->connect($proxy_host, $proxy_port, $timeout_at);
} else {
($sock, $err_reason) = $self->connect_ssl_over_proxy(
$proxy_host, $proxy_port, $host, $port, $timeout_at);
}
} else {
my $flags = fcntl( $sock, F_GETFL, 0 )
or Carp::croak("Cannot get flags for the socket: $!");
$flags = fcntl( $sock, F_SETFL, $flags | O_NONBLOCK )
or Carp::croak("Cannot set flags for the socket: $!");
}

{
# no buffering
my $orig = select();
select($sock); $|=1;
select($orig);
if ($scheme eq 'http') {
($sock, $err_reason)
= $self->connect($host, $port, $timeout_at);
} else {
($sock, $err_reason)
= $self->connect_ssl($host, $port, $timeout_at);
}
}

binmode $sock;
return $self->_r500($err_reason)
unless $sock;
}

# write request
Expand Down Expand Up @@ -489,18 +472,25 @@ sub connect :method {
my($self, $host, $port, $timeout_at) = @_;
my $sock;
my $iaddr = inet_aton($host)
or Carp::croak("Cannot resolve host name: $host, $!");
or return (undef, Carp::croak("Cannot resolve host name: $host, $!"));
my $sock_addr = pack_sockaddr_in($port, $iaddr);

RETRY:
socket($sock, PF_INET, SOCK_STREAM, 0)
or Carp::croak("Cannot create socket: $!");
if (! connect($sock, $sock_addr)) {
_set_sockopts($sock);
if (connect($sock, $sock_addr)) {
# connected
} elsif ($! == EINPROGRESS) {
$self->do_select(1, $sock, $timeout_at)
or return (undef, "Cannot connect to ${host}:${port}: timeout");
# connected
} else {
if ($! == EINTR && ! $self->{abort_on_eintr}->()) {
close $sock;
goto RETRY;
}
Carp::croak("Cannot connect to ${host}:${port}: $!");
return (undef, "Cannot connect to ${host}:${port}: $!");
}
return $sock;
}
Expand All @@ -509,11 +499,19 @@ sub connect :method {
# You can override this methond in your child class, if you want to use Crypt::SSLeay or some other library.
# @return file handle like object
sub connect_ssl {
my ($self, $host, $port) = @_;
my ($self, $host, $port, $timeout_at) = @_;
_requires('IO/Socket/SSL.pm', 'SSL');

return IO::Socket::SSL->new( PeerHost => $host, PeerPort => $port )
or Carp::croak("Cannot create SSL connection: $!");
my $timeout = $timeout_at - time;
return (undef, "Cannot create SSL connection: timeout")
if $timeout <= 0;
my $sock = IO::Socket::SSL->new(
PeerHost => $host,
PeerPort => $port,
Timeout => $timeout,
) or return (undef, "Cannot create SSL connection: $!");
_set_sockopts($sock);
$sock;
}

sub connect_ssl_over_proxy {
Expand All @@ -530,18 +528,20 @@ sub connect_ssl_over_proxy {
my $read = $self->read_timeout($sock,
\$buf, $self->{bufsize}, length($buf), $timeout_at);
if (not defined $read) {
Carp::croak("Cannot read proxy response: " . _strerror_or_timeout());
return (undef, "Cannot read proxy response: " . _strerror_or_timeout());
} elsif ( $read == 0 ) { # eof
Carp::croak("Unexpected EOF while reading proxy response");
return (undef, "Unexpected EOF while reading proxy response");
} elsif ( $buf !~ /^HTTP\/1.[01] 200 Connection established\015\012/ ) {
Carp::croak("Invalid HTTP Response via proxy");
return (undef, "Invalid HTTP Response via proxy");
}

my $timeout = $timeout_at - time;
Carp::croak("Cannot start SSL connection: timoeut")
return (undef, "Cannot start SSL connection: timeout")
if $timeout_at <= 0;
IO::Socket::SSL->start_SSL( $sock, Timeout => $timeout )
or Carp::croak("Cannot start SSL connection: " . _strerror_or_timeout());
or return (
undef, "Cannot start SSL connection: " . _strerror_or_timeout());
_set_sockopts($sock); # just in case (20101118 kazuho)
}

sub _read_body_chunked {
Expand Down Expand Up @@ -733,6 +733,32 @@ sub _strerror_or_timeout {
$! != 0 ? "$!" : 'timeout';
}

sub _set_sockopts {
my $sock = shift;

setsockopt( $sock, IPPROTO_TCP, TCP_NODELAY, 1 )
or Carp::croak("Failed to setsockopt(TCP_NODELAY): $!");
if (WIN32) {
my $tmp = 1;
ioctl( $sock, 0x8004667E, \$tmp )
or Carp::croak("Cannot set flags for the socket: $!");
} else {
my $flags = fcntl( $sock, F_GETFL, 0 )
or Carp::croak("Cannot get flags for the socket: $!");
$flags = fcntl( $sock, F_SETFL, $flags | O_NONBLOCK )
or Carp::croak("Cannot set flags for the socket: $!");
}

{
# no buffering
my $orig = select();
select($sock); $|=1;
select($orig);
}

binmode $sock;
}

# You can override this method if you want to use more powerful matcher.
sub match_no_proxy {
my ( $self, $no_proxy, $host ) = @_;
Expand Down
57 changes: 57 additions & 0 deletions t/200_online/05_connect_error.t
@@ -0,0 +1,57 @@
use strict;
use warnings;

use Furl::HTTP;
use Test::More;
use Time::HiRes qw(time);

my $n = shift(@ARGV) || 2;

# TODO add proxy tests

note 'refused error';
{
my $furl = Furl::HTTP->new(timeout => 60);
for my $scheme (qw(http https)) {
for (1 .. $n) {
my $start_at = time;
my (undef, $code, $msg, $headers, $content) =
$furl->request(
host => '255.255.255.255',
port => 80,
scheme => $scheme,
path_query => '/foo',
);
my $elapsed = time - $start_at;
is $code, 500, "request/$scheme/$_";
is $msg, 'Internal Server Error';
is ref($headers), 'ARRAY';
ok $content, "content: $content";
ok $elapsed < 0.5;
}
}
}

note 'timeout error';
# Timeout parameter of IO::Socket::SSL does not seem to be accurate, so only test http
for my $scheme (qw(http)) {
for my $timeout (1.5, 4, 8) {
my $furl = Furl::HTTP->new(timeout => $timeout);
my $start_at = time;
my (undef, $code, $msg, $headers, $content) =
$furl->request(
host => 'google.com',
port => 81,
scheme => $scheme,
path_query => '/foo',
);
my $elapsed = time - $start_at;
is $code, 500, "request/$scheme/timeout/$timeout";
is $msg, 'Internal Server Error';
is ref($headers), 'ARRAY';
ok $content, "content: $content";
ok $timeout - 0.1 <= $elapsed && $elapsed <= $timeout + 1, "elapsed: $elapsed";
}
}

done_testing;

0 comments on commit 29e3c8e

Please sign in to comment.