[ruby-core:81311] [Ruby trunk Feature#13396][Assigned] Net::HTTP has no write timeout

From: naruse@...
Date: 2017-05-20 18:48:03 UTC
List: ruby-core #81311
Issue #13396 has been updated by naruse (Yui NARUSE).

Status changed from Open to Assigned
Assignee set to naruse (Yui NARUSE)

The concept Net::HTTP#write_timeout sounds fine.

> However adding a Timeout.timeout call around req.exec did work.

Timeou.timeout should be avoided because it sometimes caused trouble and now eliminated.
(see also https://github.com/ruby/ruby/pull/899)

> I would also like to add native timeout support to IO.copy_stream

Agree.

> Note: I checked your commit c789ab8df5c8d6e0643ccd481f748014f0066345

You may know, https://github.com/ruby/ruby/pull/1575.patch and https://github.com/ruby/ruby/pull/1575.diff are also available.
They are written as LINK rel="alternate" in the HTML.

> > I tried setting setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, ...) on the client socket, but without success. However adding a Timeout.timeout call around req.exec did work.

> Under Linux, SO_SNDTIMEO won't work if the the socket was set to
nonblocking which would cause Ruby to wait with ppoll (select on other platforms).

Like this?
Maybe though IO#write seems to be kept as is.

```diff
@@ -1340,6 +1348,17 @@ io_binwrite(VALUE str, const char *ptr, long len, rb_io_t *fptr, int nosync)
             n -= r;
             errno = EAGAIN;
        }
+#ifdef SO_SNDTIMEO
+       if (errno == EAGAIN && is_socket(fptr->fd, fptr->pathv)) {
+           /* Even if EAGAIN, it may be caused by timeout */
+           struct timeval timeout;
+           socklen_t sz = sizeof(timeout);
+           if (getsockopt(fptr->fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, &sz) == 0 &&
+                   timeout.tv_sec !=0 || timeout.tv_usec != 0) {
+               return r;
+           }
+       }
+#endif
        if (r == -2L)
            return -1L;
         if (rb_io_wait_writable(fptr->fd)) {
```

----------------------------------------
Feature #13396: Net::HTTP has no write timeout
https://bugs.ruby-lang.org/issues/13396#change-64992

* Author: byroot (Jean Boussier)
* Status: Assigned
* Priority: Normal
* Assignee: naruse (Yui NARUSE)
* Target version: 
----------------------------------------
When sending a large request to an unresponsive server, `Net::HTTP` can hang pretty much forever.

```ruby
# server.rb
require 'socket'

server = TCPServer.new('localhost', 2345)
loop do
  socket = server.accept
end

```

```ruby
# client.rb
require 'net/http'

connection = Net::HTTP.new('localhost', 2345)
connection.open_timeout = 1
connection.read_timeout = 3
connection.start

post = Net::HTTP::Post.new('/')
body = (('a' * 1023) + "\n") * 5_000
post.body = body

puts "Sending #{body.bytesize} bytes"
connection.request(post)
```

The above code will hang forever on all system I tested it on (OSX / Linux 3.19).

The issue only trigger once the request body is above a certain threshold. That threshold depends on the system, I assume it's due to the system's TCP settings, but a request over 4MB will trigger the issue consistently.

I assume it happens when the request is bigger than the socket buffer. 

It's stuck on the following path:

```
/net/protocol.rb:211:in `write': Interrupt
/net/protocol.rb:211:in `write0'
/net/protocol.rb:185:in `block in write'
/net/protocol.rb:202:in `writing'
/net/protocol.rb:184:in `write'
/net/http/generic_request.rb:188:in `send_request_with_body'
/net/http/generic_request.rb:121:in `exec'
/net/http.rb:1435:in `block in transport_request'
/net/http.rb:1434:in `catch'
/net/http.rb:1434:in `transport_request'
/net/http.rb:1407:in `request'
```

I tried setting `setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, ...)` on the client socket, but without success. However adding a `Timeout.timeout` call around `req.exec` did work.



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>

In This Thread

Prev Next