Repository: rack/rack (Ruby)

Issue:
`MethodOverride` middleware silently eats invalid parameters.
See also <https://github.com/rack/rack/pull/2006>. cc @byroot @jeremyevans 

The current implementation of `method_override_param` effectively eats invalid parameters.

```ruby
    def method_override_param(req)
      req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
    rescue Utils::InvalidParameterError, Utils::ParameterTypeError
      req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
    rescue EOFError
      req.get_header(RACK_ERRORS).puts "Bad request content body"
    end
```

The first time this is called, an exception is raised and caught. However, the params is cached, and so no follow-up call to params doesn't appear to fail with the same error. This can change the behaviour of web applications which expect `Rack::Request#POST` or `#params` to raise an exception on invalid input.

I'm not sure what the ideal behaviour should be. Maybe we need to cache the exception and raise it in subsequent calls?

Example reproduction:

```ruby
#!/usr/bin/env ruby

require 'stringio'
require 'rack/request'

env = {
  'REQUEST_METHOD' => 'POST',
  'PATH_INFO' => '/foo',
  'rack.input' => StringIO.new('invalid=bar&invalid[foo]=bar'),
  'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded",
}

2.times do
  begin
    pp params: Rack::Request.new(env).POST
  rescue => error
    pp error: error
  end
end
```

Output:

```
["/home/samuel/.gem/ruby/3.2.0/gems/rack-3.0.3/lib/rack/query_parser.rb:79:in `block in parse_nested_query'",
 "/home/samuel/.gem/ruby/3.2.0/gems/rack-3.0.3/lib/rack/query_parser.rb:76:in `each'",
 "/home/samuel/.gem/ruby/3.2.0/gems/rack-3.0.3/lib/rack/query_parser.rb:76:in `parse_nested_query'",
 "/home/samuel/.gem/ruby/3.2.0/gems/rack-3.0.3/lib/rack/request.rb:648:in `parse_query'",
 "/home/samuel/.gem/ruby/3.2.0/gems/rack-3.0.3/lib/rack/request.rb:512:in `POST'",
 "/home/samuel/Desktop/request.rb:15:in `block in <main>'",
 "/home/samuel/Desktop/request.rb:13:in `times'",
 "/home/samuel/Desktop/request.rb:13:in `<main>'"]
{:error=>
  #<Rack::QueryParser::ParameterTypeError: expected Hash (got String) for param `invalid'>}
{:params=>{}}
```


Discussion:
Part of the problem here is probably to do with the fact that the input body is no longer rewindable. If it was, it would probably be rewound and on parsing a 2nd time, the exception would be raised. I think the correct solution here is to cache the exception.
> I think the correct solution here is to cache the exception.

Yes.



Patch:
diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb
index 6aa9281f6..13365935b 100644
--- a/lib/rack/constants.rb
+++ b/lib/rack/constants.rb
@@ -55,6 +55,7 @@ module Rack
   RACK_REQUEST_FORM_INPUT             = 'rack.request.form_input'
   RACK_REQUEST_FORM_HASH              = 'rack.request.form_hash'
   RACK_REQUEST_FORM_VARS              = 'rack.request.form_vars'
+  RACK_REQUEST_FORM_ERROR             = 'rack.request.form_error'
   RACK_REQUEST_COOKIE_HASH            = 'rack.request.cookie_hash'
   RACK_REQUEST_COOKIE_STRING          = 'rack.request.cookie_string'
   RACK_REQUEST_QUERY_HASH             = 'rack.request.query_hash'
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 6641b5fcd..40922a219 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -496,26 +496,35 @@ def GET
       # This method support both application/x-www-form-urlencoded and
       # multipart/form-data.
       def POST
-        if get_header(RACK_INPUT).nil?
-          raise "Missing rack.input"
-        elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
-          get_header(RACK_REQUEST_FORM_HASH)
-        elsif form_data? || parseable_data?
-          unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
-            form_vars = get_header(RACK_INPUT).read
-
-            # Fix for Safari Ajax postings that always append \0
-            # form_vars.sub!(/\0\z/, '') # performance replacement:
-            form_vars.slice!(-1) if form_vars.end_with?("\0")
-
-            set_header RACK_REQUEST_FORM_VARS, form_vars
-            set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
+        if error = get_header(RACK_REQUEST_FORM_ERROR)
+          raise error.class, error.message, cause: error.cause
+        end
+
+        begin
+          if get_header(RACK_INPUT).nil?
+            raise "Missing rack.input"
+          elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
+            get_header(RACK_REQUEST_FORM_HASH)
+          elsif form_data? || parseable_data?
+            unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
+              form_vars = get_header(RACK_INPUT).read
+
+              # Fix for Safari Ajax postings that always append \0
+              # form_vars.sub!(/\0\z/, '') # performance replacement:
+              form_vars.slice!(-1) if form_vars.end_with?("\0")
+
+              set_header RACK_REQUEST_FORM_VARS, form_vars
+              set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
+            end
+            set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+            get_header RACK_REQUEST_FORM_HASH
+          else
+            set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+            set_header(RACK_REQUEST_FORM_HASH, {})
           end
-          set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
-          get_header RACK_REQUEST_FORM_HASH
-        else
-          set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
-          set_header(RACK_REQUEST_FORM_HASH, {})
+        rescue => error
+          set_header(RACK_REQUEST_FORM_ERROR, error)
+          raise
         end
       end
 


Test patch:
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 750639deb..9fed5029a 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -1218,6 +1218,22 @@ def initialize(*)
       req.media_type_params['weird'].must_equal 'lol"'
   end
 
+  it "returns the same error for invalid post inputs" do
+    env = {
+      'REQUEST_METHOD' => 'POST',
+      'PATH_INFO' => '/foo',
+      'rack.input' => StringIO.new('invalid=bar&invalid[foo]=bar'),
+      'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded",
+    }
+    
+    2.times do
+      # The actual exception type here is unimportant - just that it fails.
+      assert_raises(Rack::Utils::ParameterTypeError) do
+        Rack::Request.new(env).POST
+      end
+    end
+  end
+
   it "parse with junk before boundary" do
     # Adapted from RFC 1867.
     input = <<EOF