Repository: puma/puma (Ruby)

Issue:
Puma sends empty response bodies even when low-level handler is specified
**Describe the bug**

Puma's README [describes the `lowlevel_error_handler` as a way to specify how Puma should react in error conditions outside of the context of your application][0]. Specifically, the docs show that you can return a Rack response with the expectation that Puma will write that response to the client socket.

In practice, though, Puma only writes that response in one particular kind of low-level error, [`server.rb:737`](https://github.com/puma/puma/blob/v4.3.5/lib/puma/server.rb#L737). There are around [six other places][1] where the `lowlevel_error_handler` is invoked, but the return value is just discarded. In these cases, Puma writes the HTTP status line itself to the socket and closes it, without writing headers or a response body.

Since the `lowlevel_error_handler` is invoked in these cases, it's still better than nothing since application developers can still be notified of errors happening if they're reporting the error to their error collection system. But Puma offers no way to customize the response code, response headers, or response body in these cases. Clients just get blank responses.

[0]: https://github.com/puma/puma/blob/0f718d516b92cd8bc4120c543b06792b22ac20bb/README.md#error-handling
[1]: https://github.com/puma/puma/commit/7577a27b5a16fbdd62b5d2162396894c01c7d31b

**Puma config:**

```ruby
lowlevel_error_handler do |e|
  puts "lowlevel_error_handler called"
  [500, {}, ["An error has occurred, and engineers have been informed. Please reload the page. If you continue to have problems, contact support@example.com\n"]]
end
```

**Expected behavior**

Puma writes the Rack response specified in the return value of the `lowlevel_error_handler` in every case that it's invoked.

**Desktop (please complete the following information):**
 - OS: MacOS, Linux
 - Puma Version: 4.3.5



Discussion: @cjlarose did you start on this? would be useful to be able to rescue `Puma::HttpParserError`s IMHO and not have Puma log them (the requests are most often abuse)
Haven't started on this myself. I think when I wrote it up originally, my team was actually finding that puma was giving 5xx error responses to some machine clients (other services in our system), but our error tracker didn't have anything relevant. I think it was related to a problem with phased restarts.

What I want is to at least give the option to application developers to handle the error and respond with their own custom error response if they want. Right now, there a few places where application developers can't even hook into the errors and know that they're happening.

I agree it's _normally_ misbehaving HTTP clients that causes these kinds of errors, but in some cases we've found that legitimate users can run into these problems and it'd be nice to be able to report them.
Re-opening this issue as #2731 was reverted in https://github.com/puma/puma/pull/2809
I took a stab at this here https://github.com/puma/puma/pull/3094. The approach is quite the same as https://github.com/puma/puma/pull/2731, except that I re-use the `prepare_response` method. Hence no changes in the `Puma::Request` module. 
Let me know if there's anything else needed to be tackled.



Fix (patch):
diff --git a/lib/puma/server.rb b/lib/puma/server.rb
index 87da8aa45a..807213a008 100644
--- a/lib/puma/server.rb
+++ b/lib/puma/server.rb
@@ -476,7 +476,7 @@ def process_client(client)
         end
         true
       rescue StandardError => e
-        client_error(e, client)
+        client_error(e, client, requests)
         # The ensure tries to close +client+ down
         requests > 0
       ensure
@@ -504,22 +504,22 @@ def with_force_shutdown(client, &block)
     # :nocov:
 
     # Handle various error types thrown by Client I/O operations.
-    def client_error(e, client)
+    def client_error(e, client, requests = 1)
       # Swallow, do not log
       return if [ConnectionError, EOFError].include?(e.class)
 
-      lowlevel_error(e, client.env)
       case e
       when MiniSSL::SSLError
+        lowlevel_error(e, client.env)
         @log_writer.ssl_error e, client.io
       when HttpParserError
-        client.write_error(400)
+        response_to_error(client, requests, e, 400)
         @log_writer.parse_error e, client
       when HttpParserError501
-        client.write_error(501)
+        response_to_error(client, requests, e, 501)
         @log_writer.parse_error e, client
       else
-        client.write_error(500)
+        response_to_error(client, requests, e, 500)
         @log_writer.unknown_error e, nil, "Read"
       end
     end
@@ -541,10 +541,17 @@ def lowlevel_error(e, env, status=500)
         backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
         [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
       else
-        [status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
+        [status, {}, [""]]
       end
     end
 
+    def response_to_error(client, requests, err, status_code)
+      status, headers, res_body = lowlevel_error(err, client.env, status_code)
+      prepare_response(status, headers, res_body, requests, client)
+      client.write_error(status_code)
+    end
+    private :response_to_error
+
     # Wait for all outstanding requests to finish.
     #
     def graceful_shutdown