Skip to content

Commit

Permalink
Add invalid_request_body plugin for custom handling of invalid reques…
Browse files Browse the repository at this point in the history
…t bodies
  • Loading branch information
jeremyevans committed Aug 17, 2023
1 parent 1d69b7d commit 6381bc5
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
= master

* Add invalid_request_body plugin for custom handling of invalid request bodies (jeremyevans)

* Warn when defining method that expects 1 argument when block requires multiple arguments when :check_arity option is set to :warn (jeremyevans)

* Implement the match_hooks plugin using the match_hook_args plugin (jeremyevans)
Expand Down
107 changes: 107 additions & 0 deletions lib/roda/plugins/invalid_request_body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen-string-literal: true

#
class Roda
module RodaPlugins
# The invalid_request_body plugin allows for custom handling of invalid request
# bodies. Roda uses Rack for parsing request bodies, so by default, any
# invalid request bodies would result in Rack raising an exception, and the
# exception could change for different reasons the request body is invalid.
# This plugin overrides RodaRequest#POST (which parses parameters from request
# bodies), and if parsing raises an exception, it allows for custom behavior.
#
# If you want to treat an invalid request body as the submission of no parameters,
# you can use the :empty_hash argument when loading the plugin:
#
# plugin :invalid_request_body, :empty_hash
#
# If you want to return a empty 400 (Bad Request) response if an invalid request
# body is submitted, you can use the :empty_400 argument when loading the plugin:
#
# plugin :invalid_request_body, :empty_400
#
# If you want to raise a Roda::RodaPlugins::InvalidRequestBody::Error exception
# if an invalid request body is submitted (which makes it easier to handle these
# exceptions when using the error_handler plugin), you can use the :raise argument
# when loading the plugin:
#
# plugin :invalid_request_body, :raise
#
# For custom behavior, you can pass a block when loading the plugin. The block
# is called with the exception Rack raised when parsing the body. The block will
# be used to define a method in the application's RodaRequest class. It can either
# return a hash of parameters, or you can raise a different exception, or you
# can halt processing and return a response:
#
# plugin :invalid_request_body do |exception|
# # To treat the exception raised as a submitted parameter
# {body_error: e}
# end
module InvalidRequestBody
# Exception class raised for invalid request bodies.
Error = Class.new(RodaError)

# Set the action to use (:empty_400, :empty_hash, :raise) for invalid request bodies,
# or use a block for custom behavior.
def self.configure(app, action=nil, &block)
if action
if block
raise RodaError, "cannot provide both block and action when loading invalid_request_body plugin"
end

method = :"handle_invalid_request_body_#{action}"
unless RequestMethods.private_method_defined?(method)
raise RodaError, "invalid invalid_request_body action provided: #{action}"
end

app::RodaRequest.send(:alias_method, :handle_invalid_request_body, method)
elsif block
app::RodaRequest.class_eval do
define_method(:handle_invalid_request_body, &block)
alias handle_invalid_request_body handle_invalid_request_body
end
else
raise RodaError, "must provide block or action when loading invalid_request_body plugin"
end

app::RodaRequest.send(:private, :handle_invalid_request_body)
end

module RequestMethods
# Handle invalid request bodies as configured if the default behavior
# raises an exception.
def POST
super
rescue => e
handle_invalid_request_body(e)
end

private

# Return an empty 400 HTTP response for invalid request bodies.
def handle_invalid_request_body_empty_400(e)
response.status = 400
headers = response.headers
headers.clear
headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html'
headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
throw :halt, response.finish_with_body([])
end

# Treat invalid request bodies by using an empty hash as the
# POST params.
def handle_invalid_request_body_empty_hash(e)
{}
end

# Raise a specific error for all invalid request bodies,
# to allow for easy rescuing using the error_handler plugin.
def handle_invalid_request_body_raise(e)
raise Error, e.message
end
end
end

register_plugin(:invalid_request_body, InvalidRequestBody)
end
end
54 changes: 54 additions & 0 deletions spec/plugin/invalid_request_body_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require_relative "../spec_helper"

describe "invalid_request_body plugin" do
def invalid_request_body_app(*args, &block)
app(:bare) do
plugin :invalid_request_body, *args, &block
route{|r| r.POST.inspect}
end
end
content_type = 'multipart/form-data; boundary=foobar'
valid_body = "--foobar\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\ny\r\n--foobar--"
define_method :valid_request_hash do
{"REQUEST_METHOD"=>'POST', 'CONTENT_TYPE'=>content_type, 'CONTENT_LENGTH'=>valid_body.bytesize.to_s, 'rack.input'=>rack_input(valid_body)}
end
define_method :invalid_request_hash do
{"REQUEST_METHOD"=>'POST', 'CONTENT_TYPE'=>content_type, 'CONTENT_LENGTH'=>'100', 'rack.input'=>rack_input}
end

it "supports :empty_400 plugin argument" do
invalid_request_body_app(:empty_400)
body(valid_request_hash).must_equal '{"x"=>"y"}'
req(invalid_request_hash).must_equal [400, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'0'}, []]
end

it "supports :empty_hash plugin argument" do
invalid_request_body_app(:empty_hash)
body(valid_request_hash).must_equal '{"x"=>"y"}'
req(invalid_request_hash).must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'2'}, ['{}']]
end

it "supports :raise plugin argument" do
invalid_request_body_app(:raise)
body(valid_request_hash).must_equal '{"x"=>"y"}'
proc{req(invalid_request_hash)}.must_raise Roda::RodaPlugins::InvalidRequestBody::Error
end

it "supports plugin block argument" do
invalid_request_body_app{|e| {'y'=>"x"}}
body(valid_request_hash).must_equal '{"x"=>"y"}'
body(invalid_request_hash).must_equal '{"y"=>"x"}'
end

it "raises Error if configuring plugin with invalid plugin argument" do
proc{invalid_request_body_app(:foo)}.must_raise Roda::RodaError
end

it "raises Error if configuring plugin with block and regular argument" do
proc{invalid_request_body_app(:raise){}}.must_raise Roda::RodaError
end

it "raises Error if configuring plugin without block or regular argument" do
proc{invalid_request_body_app}.must_raise Roda::RodaError
end
end
1 change: 1 addition & 0 deletions www/pages/documentation.erb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
<li><a href="rdoc/classes/Roda/RodaPlugins/DisallowFileUploads.html">disallow_file_uploads</a>: Disallow multipart file uploads.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/DropBody.html">drop_body</a>: Automatically drops response body and Content-Type/Content-Length headers for response statuses indicating no body.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/Halt.html">halt</a>: Augments request halt method for support for setting response status and/or response body.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/InvalidRequestBody.html">invalid_request_body</a>: Allows for custom handling of invalid request bodies.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/ModuleInclude.html">module_include</a>: Adds request_module and response_module class methods for adding modules/methods to request/response classes.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/PlainHashResponseHeaders.html">plain_hash_response_headers</a>: Uses plain hashes for response headers on Rack 3, for much better performance.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/R.html">r</a>: Adds r method for accessing the request, useful when r local variable is not in scope.</li>
Expand Down

0 comments on commit 6381bc5

Please sign in to comment.