NoMethodError in Client#parser_execute when the read buffer is only CRLFs (bare `\r\n\r\n` request)
Describe the bug
The rescue block in Puma::Client#parser_execute that enriches HttpParserError messages raises NoMethodError: undefined method 'split' for nil when the read buffer contains nothing but CRLFs — for example when a client sends a bare \r\n\r\n.
https://github.com/puma/puma/blob/master/lib/puma/client.rb#L314-L315
req, _ = @buffer.split "\r\n\r\n"
request_line, headers = req.split "\r\n", 2String#split discards trailing empty fields, so "\r\n\r\n".split("\r\n\r\n") returns [], req is nil, and the next line raises. The same happens for any number of repeated separators:
"".split("\r\n\r\n") # => [] -> req = nil
"\r\n\r\n".split("\r\n\r\n") # => [] -> req = nil
"\r\n\r\n\r\n\r\n".split("\r\n\r\n") # => [] -> req = nil
"\r\n\r\nfoo".split("\r\n\r\n") # => ["", "foo"] -> req = "" (fine)Worth noting that this rescue path is not limited to actual TLS-on-plaintext-port connections. The parser raises that same "…non-SSL Puma?" message for any malformed request (ext/puma_http11/puma_http11.c#L399, and Http11.java for JRuby), so e.message.include?('non-SSL') matches ordinary malformed input too.
Two consequences:
- The accurate, actionable
HttpParserError("Invalid HTTP format, parsing fails…") is replaced by aNoMethodErrorthat points at Puma internals. - Because
NoMethodErroris not anHttpParserError,Server#client_errorfalls through to theelsebranch and responds 500 instead of 400.
The reactor itself is unaffected — Server#reactor_wakeup rescues, calls client_error and closes the client — so this is not a stability problem, just a wrong status code and a lost diagnostic.
Puma config:
None; reproduced with the hello-world app from this template and no config file:
bundle exec puma -C - -b tcp://127.0.0.1:9292 hello.ruTo Reproduce
hello.ru:
run lambda { |env| [200, {"content-type" => "text/plain"}, ["Hello World"]] }Send a bare CRLFCRLF:
$ printf '\r\n\r\n' | nc 127.0.0.1 9292
HTTP/1.0 500 Internal Server Error
content-length: 573
Puma caught this error: undefined method 'split' for nil (NoMethodError)
/usr/local/bundle/gems/puma-8.0.1-java/lib/puma/client.rb:297:in 'parser_execute'
/usr/local/bundle/gems/puma-8.0.1-java/lib/puma/client.rb:264:in 'try_to_finish'
...It can also be shown at the parser level, without a server:
require "puma"
begin
Puma::HttpParser.new.execute({}, "\r\n\r\n", 0)
rescue => e
puts e.class # Puma::HttpParserError
puts e.message # Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?
p "\r\n\r\n".split("\r\n\r\n") # => [] <- req is nil in parser_execute
endExpected behavior
The original HttpParserError should surface (400), rather than a NoMethodError (500). Something like:
req, _ = @buffer.split "\r\n\r\n"
raise e if req.nil?Happy to open a PR if you'd like.
Desktop (please complete the following information):
- OS: Linux
- Puma Version: 8.0.1 (also present unchanged on
masteratlib/puma/client.rb#L314-L315, and in the 8.0.2 docs) - Ruby Version: jruby 10.0.5.0 (3.4.5) / OpenJDK 21.0.11. The failing code is plain Ruby and the Java and C parsers raise the same message, so I would expect CRuby to behave identically, though I have only reproduced it on JRuby.
How we noticed
A handful of these per day in production behind a load balancer, presumably scanners or broken clients opening a connection and sending an empty request.
Source: puma/puma