rack-contrib 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack-contrib might be problematic. Click here for more details.

data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 The Committers
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,60 @@
1
+ = Contributed Rack Middleware and Utilities
2
+
3
+ This package includes a variety of add-on components for Rack, a Ruby web server
4
+ interface:
5
+
6
+ * Rack::ETag - Automatically sets the ETag header on all String bodies.
7
+ * Rack::JSONP - Adds JSON-P support by stripping out the callback param
8
+ and padding the response with the appropriate callback format.
9
+ * Rack::LighttpdScriptNameFix - Fixes how lighttpd sets the SCRIPT_NAME
10
+ and PATH_INFO variables in certain configurations.
11
+ * Rack::Locale - Detects the client locale using the Accept-Language request
12
+ header and sets a rack.locale variable in the environment.
13
+ * Rack::MailExceptions - Rescues exceptions raised from the app and
14
+ sends a useful email with the exception, stacktrace, and contents of the
15
+ environment.
16
+ * Rack::NestedParams - parses form params with subscripts (e.g., * "post[title]=Hello")
17
+ into a nested/recursive Hash structure (based on Rails' implementation).
18
+ * Rack::PostBodyContentTypeParser - Adds support for JSON request bodies. The
19
+ Rack parameter hash is populated by deserializing the JSON data provided in
20
+ the request body when the Content-Type is application/json.
21
+ * Rack::ProcTitle - Displays request information in process title ($0) for
22
+ monitoring/inspection with ps(1).
23
+ * Rack::Profiler - Uses ruby-prof to measure request time.
24
+ * Rack::Sendfile - Enables X-Sendfile support for bodies that can be served
25
+ from file.
26
+ * Rack::TimeZone - Detects the clients timezone using JavaScript and sets
27
+ a variable in Rack's environment with the offset from UTC.
28
+ * Rack::Evil - Lets the rack application return a response to the client from any place.
29
+ * Rack::Callbacks - Implements DLS for pure before/after filter like Middlewares.
30
+
31
+ === Use
32
+
33
+ Git is the quickest way to the rack-contrib sources:
34
+
35
+ git clone git://github.com/rack/rack-contrib.git
36
+
37
+ Gems are currently available from GitHub clones:
38
+
39
+ gem install rack-rack-contrib --source=http://gems.github.com/
40
+
41
+ Requiring 'rack/contrib' will add autoloads to the Rack modules for all of the
42
+ components included. The following example shows what a simple rackup
43
+ (+config.ru+) file might look like:
44
+
45
+ require 'rack'
46
+ require 'rack/contrib'
47
+
48
+ use Rack::Profiler if ENV['RACK_ENV'] == 'development'
49
+
50
+ use Rack::ETag
51
+ use Rack::MailExceptions
52
+
53
+ run theapp
54
+
55
+ === Links
56
+
57
+ rack-contrib on GitHub:: <http://github.com/rack/rack-contrib>
58
+ Rack:: <http://rack.rubyforge.org/>
59
+ Rack On GitHub:: <http://github.org/rack/rack>
60
+ rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
data/Rakefile ADDED
@@ -0,0 +1,92 @@
1
+ # Rakefile for Rack::Contrib. -*-ruby-*-
2
+ require 'rake/rdoctask'
3
+ require 'rake/testtask'
4
+
5
+ desc "Run all the tests"
6
+ task :default => [:test]
7
+
8
+ desc "Generate RDox"
9
+ task "RDOX" do
10
+ sh "specrb -Ilib:test -a --rdox >RDOX"
11
+ end
12
+
13
+ desc "Run all the fast tests"
14
+ task :test do
15
+ sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
16
+ end
17
+
18
+ desc "Run all the tests"
19
+ task :fulltest do
20
+ sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
21
+ end
22
+
23
+ desc "Generate RDoc documentation"
24
+ Rake::RDocTask.new(:rdoc) do |rdoc|
25
+ rdoc.options << '--line-numbers' << '--inline-source' <<
26
+ '--main' << 'README' <<
27
+ '--title' << 'Rack Contrib Documentation' <<
28
+ '--charset' << 'utf-8'
29
+ rdoc.rdoc_dir = "doc"
30
+ rdoc.rdoc_files.include 'README.rdoc'
31
+ rdoc.rdoc_files.include 'RDOX'
32
+ rdoc.rdoc_files.include('lib/rack/*.rb')
33
+ rdoc.rdoc_files.include('lib/rack/*/*.rb')
34
+ end
35
+ task :rdoc => ["RDOX"]
36
+
37
+
38
+ # PACKAGING =================================================================
39
+
40
+ # load gemspec like github's gem builder to surface any SAFE issues.
41
+ require 'rubygems/specification'
42
+ $spec = eval(File.read('rack-contrib.gemspec'))
43
+
44
+ def package(ext='')
45
+ "pkg/rack-contrib-#{$spec.version}" + ext
46
+ end
47
+
48
+ desc 'Build packages'
49
+ task :package => %w[.gem .tar.gz].map {|e| package(e)}
50
+
51
+ desc 'Build and install as local gem'
52
+ task :install => package('.gem') do
53
+ sh "gem install #{package('.gem')}"
54
+ end
55
+
56
+ directory 'pkg/'
57
+
58
+ file package('.gem') => %w[pkg/ rack-contrib.gemspec] + $spec.files do |f|
59
+ sh "gem build rack-contrib.gemspec"
60
+ mv File.basename(f.name), f.name
61
+ end
62
+
63
+ file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
64
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
65
+ end
66
+
67
+ desc 'Publish gem and tarball to rubyforge'
68
+ task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
69
+ sh <<-end
70
+ rubyforge add_release rack rack-contrib #{$spec.version} #{package('.gem')} &&
71
+ rubyforge add_file rack rack-contrib #{$spec.version} #{package('.tar.gz')}
72
+ end
73
+ end
74
+
75
+ # GEMSPEC ===================================================================
76
+
77
+ file 'rack-contrib.gemspec' => FileList['{lib,test}/**','Rakefile', 'README.rdoc'] do |f|
78
+ # read spec file and split out manifest section
79
+ spec = File.read(f.name)
80
+ parts = spec.split(" # = MANIFEST =\n")
81
+ fail 'bad spec' if parts.length != 3
82
+ # determine file list from git ls-files
83
+ files = `git ls-files`.
84
+ split("\n").sort.reject{ |file| file =~ /^\./ }.
85
+ map{ |file| " #{file}" }.join("\n")
86
+ # piece file back together and write...
87
+ parts[1] = " s.files = %w[\n#{files}\n ]\n"
88
+ spec = parts.join(" # = MANIFEST =\n")
89
+ spec.sub!(/s.date = '.*'/, "s.date = '#{Time.now.strftime("%Y-%m-%d")}'")
90
+ File.open(f.name, 'w') { |io| io.write(spec) }
91
+ puts "updated #{f.name}"
92
+ end
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ # Bounce those annoying favicon.ico requests
3
+ class BounceFavicon
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env["PATH_INFO"] == "/favicon.ico"
10
+ [404, {"Content-Type" => "text/html", "Content-Length" => "0"}, []]
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ module Rack
2
+ class Callbacks
3
+ def initialize(&block)
4
+ @before = []
5
+ @after = []
6
+ instance_eval(&block) if block_given?
7
+ end
8
+
9
+ def before(middleware, *args, &block)
10
+ if block_given?
11
+ @before << middleware.new(*args, &block)
12
+ else
13
+ @before << middleware.new(*args)
14
+ end
15
+ end
16
+
17
+ def after(middleware, *args, &block)
18
+ if block_given?
19
+ @after << middleware.new(*args, &block)
20
+ else
21
+ @after << middleware.new(*args)
22
+ end
23
+ end
24
+
25
+ def run(app)
26
+ @app = app
27
+ end
28
+
29
+ def call(env)
30
+ @before.each {|c| c.call(env) }
31
+
32
+ response = @app.call(env)
33
+
34
+ @after.inject(response) {|r, c| c.call(r) }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ require 'digest/md5'
2
+
3
+ module Rack
4
+ # Automatically sets the ETag header on all String bodies
5
+ class ETag
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ status, headers, body = @app.call(env)
12
+
13
+ if !headers.has_key?('ETag') && body.is_a?(String)
14
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(body)}")
15
+ end
16
+
17
+ [status, headers, body]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module Rack
2
+ class Evil
3
+ # Lets you return a response to the client immediately from anywhere ( M V or C ) in the code.
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ catch(:response) { @app.call(env) }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Rack
2
+ # Forces garbage collection after each request.
3
+ class GarbageCollector
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @app.call(env)
10
+ ensure
11
+ GC.start
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module Rack
2
+
3
+ # A Rack middleware for providing JSON-P support.
4
+ #
5
+ # Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
6
+ #
7
+ class JSONP
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ # Proxies the request to the application, stripping out the JSON-P callback
14
+ # method and padding the response with the appropriate callback format.
15
+ #
16
+ # Changes nothing if no <tt>callback</tt> param is specified.
17
+ #
18
+ def call(env)
19
+ status, headers, response = @app.call(env)
20
+ request = Rack::Request.new(env)
21
+ response = pad(request.params.delete('callback'), response) if request.params.include?('callback')
22
+ [status, headers, response]
23
+ end
24
+
25
+ # Pads the response with the appropriate callback format according to the
26
+ # JSON-P spec/requirements.
27
+ #
28
+ # The Rack response spec indicates that it should be enumerable. The method
29
+ # of combining all of the data into a sinle string makes sense since JSON
30
+ # is returned as a full string.
31
+ #
32
+ def pad(callback, response, body = "")
33
+ response.each{ |s| body << s }
34
+ "#{callback}(#{body})"
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ # Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
3
+ # FastCGI app at "/". This middleware fixes this issue.
4
+
5
+ class LighttpdScriptNameFix
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
12
+ env["SCRIPT_NAME"] = ""
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require 'i18n'
2
+
3
+ module Rack
4
+ class Locale
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ old_locale = I18n.locale
11
+ locale = nil
12
+
13
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
14
+ if lang = env["HTTP_ACCEPT_LANGUAGE"]
15
+ lang = lang.split(",").map { |l|
16
+ l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
17
+ l.split(';q=')
18
+ }.first
19
+ locale = lang.first.split("-").first
20
+ else
21
+ locale = I18n.default_locale
22
+ end
23
+
24
+ locale = env['rack.locale'] = I18n.locale = locale.to_s
25
+ status, headers, body = @app.call(env)
26
+ headers['Content-Language'] = locale
27
+ I18n.locale = old_locale
28
+ [status, headers, body]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ require 'net/smtp'
2
+ require 'tmail'
3
+ require 'erb'
4
+
5
+ module Rack
6
+ # Catches all exceptions raised from the app it wraps and
7
+ # sends a useful email with the exception, stacktrace, and
8
+ # contents of the environment.
9
+
10
+ class MailExceptions
11
+ attr_reader :config
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ @config = {
16
+ :to => nil,
17
+ :from => ENV['USER'] || 'rack',
18
+ :subject => '[exception] %s',
19
+ :smtp => {
20
+ :server => 'localhost',
21
+ :domain => 'localhost',
22
+ :port => 25,
23
+ :authentication => :login,
24
+ :user_name => nil,
25
+ :password => nil
26
+ }
27
+ }
28
+ @template = ERB.new(TEMPLATE)
29
+ yield self if block_given?
30
+ end
31
+
32
+ def call(env)
33
+ status, headers, body =
34
+ begin
35
+ @app.call(env)
36
+ rescue => boom
37
+ # TODO don't allow exceptions from send_notification to
38
+ # propogate
39
+ send_notification boom, env
40
+ raise
41
+ end
42
+ send_notification env['mail.exception'], env if env['mail.exception']
43
+ [status, headers, body]
44
+ end
45
+
46
+ %w[to from subject].each do |meth|
47
+ define_method(meth) { |value| @config[meth.to_sym] = value }
48
+ end
49
+
50
+ def smtp(settings={})
51
+ @config[:smtp].merge! settings
52
+ end
53
+
54
+ private
55
+ def generate_mail(exception, env)
56
+ mail = TMail::Mail.new
57
+ mail.to = Array(config[:to])
58
+ mail.from = config[:from]
59
+ mail.subject = config[:subject] % [exception.to_s]
60
+ mail.date = Time.now
61
+ mail.set_content_type 'text/plain'
62
+ mail.charset = 'UTF-8'
63
+ mail.body = @template.result(binding)
64
+ mail
65
+ end
66
+
67
+ def send_notification(exception, env)
68
+ mail = generate_mail(exception, env)
69
+ smtp = config[:smtp]
70
+ env['mail.sent'] = true
71
+ return if smtp[:server] == 'example.com'
72
+
73
+ Net::SMTP.start smtp[:address], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
74
+ mail.to.each do |recipient|
75
+ server.send_message mail.to_s, mail.from, recipient
76
+ end
77
+ end
78
+ end
79
+
80
+ def extract_body(env)
81
+ if io = env['rack.input']
82
+ io.rewind if io.respond_to?(:rewind)
83
+ io.read
84
+ end
85
+ end
86
+
87
+ TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
88
+ A <%= exception.class.to_s %> occured: <%= exception.to_s %>
89
+ <% if body = extract_body(env) %>
90
+
91
+ ===================================================================
92
+ Request Body:
93
+ ===================================================================
94
+
95
+ <%= body.gsub(/^/, ' ') %>
96
+ <% end %>
97
+
98
+ ===================================================================
99
+ Rack Environment:
100
+ ===================================================================
101
+
102
+ PID: <%= $$ %>
103
+ PWD: <%= Dir.getwd %>
104
+
105
+ <%= env.to_a.
106
+ sort{|a,b| a.first <=> b.first}.
107
+ map{ |k,v| "%-25s%p" % [k+':', v] }.
108
+ join("\n ") %>
109
+
110
+ <% if exception.respond_to?(:backtrace) %>
111
+ ===================================================================
112
+ Backtrace:
113
+ ===================================================================
114
+
115
+ <%= exception.backtrace.join("\n ") %>
116
+ <% end %>
117
+ EMAIL
118
+
119
+ end
120
+ end
@@ -0,0 +1,143 @@
1
+ require 'cgi'
2
+ require 'strscan'
3
+
4
+ module Rack
5
+ # Rack middleware for parsing POST/PUT body data into nested parameters
6
+ class NestedParams
7
+
8
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
9
+ POST_BODY = 'rack.input'.freeze
10
+ FORM_INPUT = 'rack.request.form_input'.freeze
11
+ FORM_HASH = 'rack.request.form_hash'.freeze
12
+ FORM_VARS = 'rack.request.form_vars'.freeze
13
+
14
+ # supported content type
15
+ URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ if form_vars = env[FORM_VARS]
23
+ env[FORM_HASH] = parse_query_parameters(form_vars)
24
+ elsif env[CONTENT_TYPE] == URL_ENCODED
25
+ post_body = env[POST_BODY]
26
+ env[FORM_INPUT] = post_body
27
+ env[FORM_HASH] = parse_query_parameters(post_body.read)
28
+ post_body.rewind if post_body.respond_to?(:rewind)
29
+ end
30
+ @app.call(env)
31
+ end
32
+
33
+ ## the rest is nabbed from Rails ##
34
+
35
+ def parse_query_parameters(query_string)
36
+ return {} if query_string.nil? or query_string.empty?
37
+
38
+ pairs = query_string.split('&').collect do |chunk|
39
+ next if chunk.empty?
40
+ key, value = chunk.split('=', 2)
41
+ next if key.empty?
42
+ value = value.nil? ? nil : CGI.unescape(value)
43
+ [ CGI.unescape(key), value ]
44
+ end.compact
45
+
46
+ UrlEncodedPairParser.new(pairs).result
47
+ end
48
+
49
+ class UrlEncodedPairParser < StringScanner
50
+ attr_reader :top, :parent, :result
51
+
52
+ def initialize(pairs = [])
53
+ super('')
54
+ @result = {}
55
+ pairs.each { |key, value| parse(key, value) }
56
+ end
57
+
58
+ KEY_REGEXP = %r{([^\[\]=&]+)}
59
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
60
+
61
+ # Parse the query string
62
+ def parse(key, value)
63
+ self.string = key
64
+ @top, @parent = result, nil
65
+
66
+ # First scan the bare key
67
+ key = scan(KEY_REGEXP) or return
68
+ key = post_key_check(key)
69
+
70
+ # Then scan as many nestings as present
71
+ until eos?
72
+ r = scan(BRACKETED_KEY_REGEXP) or return
73
+ key = self[1]
74
+ key = post_key_check(key)
75
+ end
76
+
77
+ bind(key, value)
78
+ end
79
+
80
+ private
81
+ # After we see a key, we must look ahead to determine our next action. Cases:
82
+ #
83
+ # [] follows the key. Then the value must be an array.
84
+ # = follows the key. (A value comes next)
85
+ # & or the end of string follows the key. Then the key is a flag.
86
+ # otherwise, a hash follows the key.
87
+ def post_key_check(key)
88
+ if scan(/\[\]/) # a[b][] indicates that b is an array
89
+ container(key, Array)
90
+ nil
91
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
92
+ container(key, Hash)
93
+ nil
94
+ else # End of key? We do nothing.
95
+ key
96
+ end
97
+ end
98
+
99
+ # Add a container to the stack.
100
+ def container(key, klass)
101
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
102
+ value = bind(key, klass.new)
103
+ type_conflict! klass, value unless value.is_a?(klass)
104
+ push(value)
105
+ end
106
+
107
+ # Push a value onto the 'stack', which is actually only the top 2 items.
108
+ def push(value)
109
+ @parent, @top = @top, value
110
+ end
111
+
112
+ # Bind a key (which may be nil for items in an array) to the provided value.
113
+ def bind(key, value)
114
+ if top.is_a? Array
115
+ if key
116
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
117
+ top[-1][key] = value
118
+ else
119
+ top << {key => value}
120
+ end
121
+ push top.last
122
+ return top[key]
123
+ else
124
+ top << value
125
+ return value
126
+ end
127
+ elsif top.is_a? Hash
128
+ key = CGI.unescape(key)
129
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
130
+ top[key] ||= value
131
+ return top[key]
132
+ else
133
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
134
+ end
135
+ end
136
+
137
+ def type_conflict!(klass, value)
138
+ raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,40 @@
1
+ begin
2
+ require 'json'
3
+ rescue LoadError => e
4
+ require 'json/pure'
5
+ end
6
+
7
+ module Rack
8
+
9
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
+ # not one of the standard supported types, like <tt>application/json</tt>.
11
+ #
12
+ # TODO: Find a better name.
13
+ #
14
+ class PostBodyContentTypeParser
15
+
16
+ # Constants
17
+ #
18
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
19
+ POST_BODY = 'rack.input'.freeze
20
+ FORM_INPUT = 'rack.request.form_input'.freeze
21
+ FORM_HASH = 'rack.request.form_hash'.freeze
22
+
23
+ # Supported Content-Types
24
+ #
25
+ APPLICATION_JSON = 'application/json'.freeze
26
+
27
+ def initialize(app)
28
+ @app = app
29
+ end
30
+
31
+ def call(env)
32
+ case env[CONTENT_TYPE]
33
+ when APPLICATION_JSON
34
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
35
+ end
36
+ @app.call(env)
37
+ end
38
+
39
+ end
40
+ end