Repository: puma/puma (Ruby)

Issue:
Early hints + javascript_include_tag = Puma:ConnectionError
### Steps to reproduce

1) Procfile enables early hints eg. web: bundle exec puma -C config/puma.rb --early-hints

2) Make use of `javascript_include_tag` helper

3) Production environment on Heroku

### Expected behavior

Testing out --early-hints on Heroku, I would expect the hints to be generated as described in [Implement H2 Early Hints for Rails](https://github.com/rails/rails/pull/30744).

### Actual behavior

I'm seeing some intermittent socket timeout errors when I have --early-hints enabled on my app, which is hosted on Heroku. It's only showing up about once a day, and only in production.

Some lucky visitor's request is throwing one Puma:ConnectionError for each `javascript_include_tag` in my application layout. If one call fails, they all fail in the order they appear in the template. I've never experienced the end-result personally, leading me to guess that the visitor probably won't notice as their script tags would just load normally.

```Aug 16 12:34:37 #<Puma::ConnectionError: Socket timeout writing data> 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/puma-3.11.4/lib/puma/server.rb:972:in 'rescue in fast_write' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/puma-3.11.4/lib/puma/server.rb:963:in 'fast_write' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/puma-3.11.4/lib/puma/server.rb:609:in 'block in handle_request' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/actionpack-5.2.0/lib/action_dispatch/http/request.rb:217:in 'send_early_hints' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/actionview-5.2.0/lib/action_view/helpers/asset_tag_helper.rb:96:in 'javascript_include_tag' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/helper.rb:156:in 'block in javascript_include_tag' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/helper.rb:154:in 'map' 
Aug 16 12:34:37 /app/vendor/bundle/ruby/2.4.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/helper.rb:154:in 'javascript_include_tag' 
Aug 16 12:34:37 /app/app/views/layouts/application.html.erb:97:in 'block in _app_views_layouts_application_html_erb__4118899528439397404_70152019751760' 
Aug 16 12:34:37 /app/app/views/layouts/application.html.erb:95:in 'each' 
Aug 16 12:34:37 /app/app/views/layouts/application.html.erb:95:in '_app_views_layouts_application_html_erb__4118899528439397404_70152019751760'
```

### System configuration

**Ruby version**: 2.4.1
**Rails version**: 5.2.0
**Puma version**: 3.11.4



Discussion:
I'm also seeing this error - any ideas to fix would be greatly appreciated!
This error happens when the connection is lost before we're able to send any early hints.

We could have the same situation even without early hints if the connection is lost before any response headers are sent, however Puma swallows these errors which I think is fine.

A simple way to reproduce this is running this against your web server:
```ruby
require 'socket'

request = [
  "GET /buildings HTTP/1.1",
  "Host: 127.0.0.1:3000",
  "\r\n"
]

socket = TCPSocket.new '127.0.0.1', 3000
socket.puts request.join("\r\n")

# Close socket without reading anything back from the server...
socket.close
```

No error will appear in the client but the reported error will appear in the app logs.



Patch:
diff --git a/lib/puma/server.rb b/lib/puma/server.rb
index 150458ec74..91428de559 100644
--- a/lib/puma/server.rb
+++ b/lib/puma/server.rb
@@ -631,19 +631,23 @@ def handle_request(req, lines)
 
       if @early_hints
         env[EARLY_HINTS] = lambda { |headers|
-          fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
+          begin
+            fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
 
-          headers.each_pair do |k, vs|
-            if vs.respond_to?(:to_s) && !vs.to_s.empty?
-              vs.to_s.split(NEWLINE).each do |v|
-                fast_write client, "#{k}: #{v}\r\n"
+            headers.each_pair do |k, vs|
+              if vs.respond_to?(:to_s) && !vs.to_s.empty?
+                vs.to_s.split(NEWLINE).each do |v|
+                  fast_write client, "#{k}: #{v}\r\n"
+                end
+              else
+                fast_write client, "#{k}: #{vs}\r\n"
               end
-            else
-              fast_write client, "#{k}: #{vs}\r\n"
             end
-          end
 
-          fast_write client, "\r\n".freeze
+            fast_write client, "\r\n".freeze
+          rescue ConnectionError
+            # noop, if we lost the socket we just won't send the early hints
+          end
         }
       end
 


Test patch:
diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb
index 73a7190e40..658e4a6c4e 100644
--- a/test/test_puma_server.rb
+++ b/test/test_puma_server.rb
@@ -189,6 +189,32 @@ def test_early_hints_works
     assert_equal expected_data, data
   end
 
+  def test_early_hints_are_ignored_if_connection_lost
+    app = proc { |env|
+      env['rack.early_hints'].call("Link" => "</script.js>; rel=preload")
+      [200, { "X-Hello" => "World" }, ["Hello world!"]]
+    }
+
+    events = Puma::Events.strings
+    server = Puma::Server.new app, events
+    def server.fast_write(*args)
+      raise Puma::ConnectionError
+    end
+    server.add_tcp_listener @host, @port
+    server.early_hints = true
+    server.run
+
+    # This request will cause the server to try and send early hints
+    sock = TCPSocket.new @host, server.connected_port
+    sock << "HEAD / HTTP/1.0\r\n\r\n"
+
+    # Give the server some time to try to write (and fail)
+    sleep 0.1
+
+    # Expect no errors in stderr
+    assert events.stderr.pos.zero?, "Server didn't swallow the connection error"
+  end
+
   def test_early_hints_is_off_by_default
     @server.app = proc { |env|
      assert_nil env['rack.early_hints']