Skip to content

Better handling of multipart parsing when request is missing body.#1997

Closed
ioquatix wants to merge 2 commits into
mainfrom
invalid-request
Closed

Better handling of multipart parsing when request is missing body.#1997
ioquatix wants to merge 2 commits into
mainfrom
invalid-request

Conversation

@ioquatix

@ioquatix ioquatix commented Dec 16, 2022

Copy link
Copy Markdown
Member
  • Introduce module Rack::BadRequest for unified exception handling.
  • Make env['rack.input'] optional.

rack.input feels like a hangover from the CGI days when $stdin was always present and open. But in modern servers, we know that there is no entity body in most cases where it's relevant. However, we still need to allocate something which fails on the first call to read (EOFError, IOError or something else). There is actually no way to detect that there is no entity body. So, I'd like to fix that, as it makes handling the multipart error more straight forward.

If you think there are any other Rack error classes that should include BadRequest, please feel free to suggest it.

Fixes #1994. Fixes #1995. Fixes #1996.

Comment thread lib/rack/request.rb
Comment thread test/spec_mock_request.rb
@ioquatix

ioquatix commented Dec 16, 2022

Copy link
Copy Markdown
Member Author

@MSP-Greg @dentarg @nateberkopec can I please get your feedback on this change as you would be expected to add rescue Rack::BadRequest and map that to a 400 error.

begin
  status, headers, body = @app.call(env)
rescue Rack::BadRequest => error
  status = 400
  headers = {'content-type' => 'text/plain'}
  body = [error.to_s]
end

It also means you should not provide rack.input if the request doesn't contain an entity body (we might need to define how to detect this for HTTP/1.x and 2+ because the semantics aren't identical).

@MSP-Greg

Copy link
Copy Markdown
Contributor

LGTM

If you think there are any other Rack error classes that should include BadRequest, please feel free to suggest it.

If that's a possibility, might Rack::BadRequest have some way of providing the status, headers & body? Maybe have it respond to call, just like an app?

@jeremyevans

Copy link
Copy Markdown
Contributor

@MSP-Greg @dentarg @nateberkopec can I please get your feedback on this change as you would be expected to add rescue Rack::BadRequest and map that to a 400 error.

I think would be expected to is better phrased as can. Neither server, middleware, frameworks, or applications should have to require rack the library to be rack compatible.

Personally, I don't think servers should directly be rescuing the exception. We should probably ship a middleware that does the handling (maybe Rack::BadRequest::Middleware), which servers/frameworks/applications that want to handle this such exceptions automatically can use.

If that's a possibility, might Rack::BadRequest have some way of providing the status, headers & body? Maybe have it respond to call, just like an app?

I don't think we should do that. Anything that could provide the status, headers, and body should just return the expected rack response array instead of raising an exception and relying on something else to do the translation.

@ioquatix

ioquatix commented Dec 16, 2022

Copy link
Copy Markdown
Member Author

@jeremyevans I don't disagree with your points. I agree servers should be able to deal with Rack as an interface without needing rack the gem. But I don't know what a better alternative is.

Rack as an interface is different from all the other stuff like Rack::Request and Rack::Response. The only thing which constitutes "rack the interface" is the spec. But we are talking about Rack::Request and Rack::Multipart which I don't consider part of "Rack the interface". In that regard, I agree that a server should not need to know about Rack::BadRequest.

Neither server, middleware, frameworks, or applications should have to require rack the library to be rack compatible.

Sure, and they can just blow up with a 500 error if that's their preference because there is no way to handle this case generally without having some class of exceptions that are "The request did not meet the requirements" vs "Something unexpected happened".

Personally, I don't think servers should directly be rescuing the exception.

How, in your opinion, should we turn this error into a 400 Bad Request error?

The only other option is some kind of middleware, but is that really good enough? As it stands, it feels like some parts of Rack::Request would need to signal "This input environment is bad/invalid" and we'd want to communicate that with a 400-status rather than a 500-status.

@jeremyevans

Copy link
Copy Markdown
Contributor

Personally, I don't think servers should directly be rescuing the exception.

How, in your opinion, should we turn this error into a 400 Bad Request error?

The only other option is some kind of middleware, but is that really good enough?

Directly after the sentence you quoted, I wrote We should probably ship a middleware that does the handling, which indicates I considered it good enough.

As it stands, it feels like some parts of Rack::Request would need to signal "This input environment is bad/invalid" and we'd want to communicate that with a 400-status rather than a 500-status.

Code that uses Rack::Multipart for parsing (or other parts of Rack where this exception could be used) should have no issues using Rack::BadRequest::Middleware to handle it at the middleware level.

@ioquatix

Copy link
Copy Markdown
Member Author

Directly after the sentence you quoted, I wrote We should probably ship a middleware that does the handling, which indicates I considered it good enough.

Sure, I understand your position, but I don't consider it good enough from a developer UX POV. Because it won't work by default, it's another overhead users need to deal with, and it's version specific (although everything here is version specific, at least servers can hide the complexity and application users don't need to deal with it).

@MSP-Greg

Copy link
Copy Markdown
Contributor

From RFC 9110 15.5 "Client Error 4xx":

"the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition. These status codes are applicable to any request method. User agents SHOULD display any included representation to the user."

Maybe it's best left to the app, rather than Rack?

@nateberkopec

Copy link
Copy Markdown

"No one has to require rack" feels like something that's too useful to lose.

@ioquatix

Copy link
Copy Markdown
Member Author

Well, I'm also fine with this just being a 500 error. But that won't solve the original problem.

@dentarg

dentarg commented Dec 16, 2022

Copy link
Copy Markdown
Contributor

I think it is okay if Rack just raises some error that web frameworks can rescue and return whatever status code they want for it. It is of course nice if there's a middleware included with Rack that can handle them automatically if you are fine with the responses that middleware makes.

I'll comment on the suggestion in #1996 where BadRequest has an attribute status. We got this bug report in Sinatra sinatra/sinatra#1518 and indeed the bug was with Sinatra (only checking if certain method existed), but seeing that bug I would be a bit wary of introducing error objects with such common method names. Maybe there are other code bases making the same mistake as Sinatra.

Comment thread lib/rack/multipart/parser.rb Outdated
@catatsuy

Copy link
Copy Markdown

I think that other language implementations would often ignore this.
But if you don't want them to be processed, a new implementation would be more appropriate than the current one

@ioquatix

Copy link
Copy Markdown
Member Author

Maybe it's best left to the app, rather than Rack?

The problem with this, is that Rack is the one actually generating the errors, because Rack::Request is a library level implementation, not part of "rack the interface". We are literally providing application level constructs that are independent of the server interface, and those can signal failures due to bad request data, and we need some way to signal this so that applications can handle it correctly.

@ioquatix

ioquatix commented Jan 12, 2023

Copy link
Copy Markdown
Member Author

@jeremyevans what do you think of the proposed change?

The middleware Rack::BadRequest::Middleware can help for people who want to adopt it.

The only issue is now:

    class EmptyContentError < ::EOFError
      include BadRequest
      # "Middleware" as a constant is defined here?
    end

Is it acceptable or should we reorganise the modules, e.g.

module Rack::BadRequest... etc

class Rack::ErrorHandler # (or ExceptionHandler?)
  def call(env)
    @app.call(env)
  rescue Rack::BadRequest
    [400 ...
  rescue
    [500 ...
  end
end

etc

@jeremyevans jeremyevans left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this PR should be split. There should be a separate PRs for the spec change from must to may, for BadRequest, and for ErrorHandler.

Comment thread lib/rack/bad_request.rb
Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/lint.rb Outdated
Comment thread lib/rack/lint.rb Outdated
Comment thread lib/rack/mock_request.rb Outdated
Comment thread test/spec_mock_request.rb
Comment thread test/spec_multipart.rb Outdated
@ioquatix ioquatix requested review from catatsuy and jeremyevans and removed request for catatsuy January 13, 2023 06:31
@ioquatix ioquatix force-pushed the invalid-request branch 5 times, most recently from 555e95a to 65782e6 Compare January 13, 2023 07:53
@ioquatix ioquatix force-pushed the invalid-request branch 3 times, most recently from 9b56e6b to d90c916 Compare January 13, 2023 08:36

@jeremyevans jeremyevans left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please split this into 3 separate pull requests, one for rack.input, one for BadRequest, and one for ErrorHandler.

Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/mock_request.rb Outdated
end

Error = BoundaryTooLongError
deprecate_constant :Error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecation at this point is too aggressive. Otherwise, how do you write code that rescues this error and works in both Rack 3.0 and 3.1 (and ideally, Rack 2)?

@ioquatix ioquatix Jan 13, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error AFAIK doesn't exist in Rack 2.x?

It was introduced here: d866dc4

Deprecations only show up with warnings enabled. We can keep it for one more minor version if you think that makes sense. How would you like to deprecate this?

I think going forward, people should probably rescue Rack::BadRequest rather than specific multipart/query_parser errors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep it until Rack 4.

@ioquatix ioquatix Jan 13, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was when should we deprecate it? I believe it can be removed in Rack 4, are you suggesting we should deprecate it it in Rack 4?

Comment thread test/spec_lint.rb
Rack::MockRequest.env_for("/", *args)
def env(options = {})
unless options.key?(:input)
options[:input] = String.new

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a smell. If we are changing rack.input to not be required, we shouldn't have the tests default to using it with an empty string.

@ioquatix ioquatix Jan 13, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lot's of tests fail without it because they assume that it's an empty string. We can rewrite those tests. However if we change that, it will make backporting (semi-unrelated changes) harder if the tests overlap since they will have diverged. This is about changing as few tests as possible - we can always rewrite the tests later once backporting is no longer expected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should give you a rough indication of how much code this change will break. If we aren't willing to modify rack's own tests to fully implement the change, it's hard to argue the benefits of the change are worth the cost.

@ioquatix ioquatix Jan 13, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we aren't willing to modify rack's own tests to fully implement the change

Well, we did modify rack's own tests, and it was just in one place since the test fixture uses this and these tests assume an empty string. That shows minimal changes were required in this specific case.

Comment thread lib/rack.rb Outdated

@jeremyevans jeremyevans left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still like this split into 3 pull requests, as previously indicated.

Also, please do not mark conversations as resolved. The reviewer should decide when a conversation has been resolved, not the person proposing the change.

Comment thread lib/rack/error_handler.rb Outdated
Comment thread lib/rack/error_handler.rb Outdated
Comment thread test/spec_lint.rb
Rack::MockRequest.env_for("/", *args)
def env(options = {})
unless options.key?(:input)
options[:input] = String.new

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should give you a rough indication of how much code this change will break. If we aren't willing to modify rack's own tests to fully implement the change, it's hard to argue the benefits of the change are worth the cost.

Comment thread lib/rack/error_handler.rb Outdated
end

Error = BoundaryTooLongError
deprecate_constant :Error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep it until Rack 4.

@ioquatix

Copy link
Copy Markdown
Member Author
      "ActionDispatch::Http::Parameters::ParseError"       => :bad_request,
      "ActionController::BadRequest"                       => :bad_request,
      "ActionController::ParameterMissing"                 => :bad_request,
      "Rack::QueryParser::ParameterTypeError"              => :bad_request,
      "Rack::QueryParser::InvalidParameterError"           => :bad_request

Rails is already doing this basically, but in a worse way.

- Introduce `module Rack::BadRequest` for unified exception handling.
- Add `BadRequest` to query parser too.
- Make `env['rack.input']` optional.

# Conflicts:
#	lib/rack/request.rb
- Introduce `module Rack::BadRequest` for unified exception handling.
- Add `BadRequest` to query parser too.
- Make `env['rack.input']` optional.

@tenderlove tenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having a BadRequest exception that people can rescue, but I agree with @jeremyevans that this PR should be split. I am having a hard time reasoning about the changes with such a big diff.

@ioquatix ioquatix closed this Jan 19, 2023
@ioquatix

Copy link
Copy Markdown
Member Author

Okay, I will split it.

@ioquatix ioquatix deleted the invalid-request branch January 19, 2023 01:49
@ioquatix

Copy link
Copy Markdown
Member Author

See #2018 and #2019 (depends on 2018).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

7 participants