rack-contrib 1.8.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +17 -10
  3. data/lib/rack/contrib.rb +3 -3
  4. data/lib/rack/contrib/access.rb +6 -4
  5. data/lib/rack/contrib/backstage.rb +3 -1
  6. data/lib/rack/contrib/bounce_favicon.rb +2 -0
  7. data/lib/rack/contrib/callbacks.rb +2 -0
  8. data/lib/rack/contrib/common_cookies.rb +16 -11
  9. data/lib/rack/contrib/config.rb +3 -15
  10. data/lib/rack/contrib/cookies.rb +2 -0
  11. data/lib/rack/contrib/csshttprequest.rb +10 -6
  12. data/lib/rack/contrib/deflect.rb +34 -32
  13. data/lib/rack/contrib/enforce_valid_encoding.rb +2 -0
  14. data/lib/rack/contrib/evil.rb +2 -0
  15. data/lib/rack/contrib/expectation_cascade.rb +3 -1
  16. data/lib/rack/contrib/garbagecollector.rb +2 -0
  17. data/lib/rack/contrib/host_meta.rb +2 -0
  18. data/lib/rack/contrib/json_body_parser.rb +85 -0
  19. data/lib/rack/contrib/jsonp.rb +8 -6
  20. data/lib/rack/contrib/lazy_conditional_get.rb +9 -0
  21. data/lib/rack/contrib/lighttpd_script_name_fix.rb +2 -0
  22. data/lib/rack/contrib/locale.rb +65 -30
  23. data/lib/rack/contrib/mailexceptions.rb +4 -2
  24. data/lib/rack/contrib/nested_params.rb +6 -121
  25. data/lib/rack/contrib/not_found.rb +3 -1
  26. data/lib/rack/contrib/post_body_content_type_parser.rb +49 -4
  27. data/lib/rack/contrib/printout.rb +2 -0
  28. data/lib/rack/contrib/proctitle.rb +2 -0
  29. data/lib/rack/contrib/profiler.rb +6 -1
  30. data/lib/rack/contrib/relative_redirect.rb +10 -5
  31. data/lib/rack/contrib/response_cache.rb +22 -11
  32. data/lib/rack/contrib/response_headers.rb +2 -0
  33. data/lib/rack/contrib/route_exceptions.rb +2 -0
  34. data/lib/rack/contrib/runtime.rb +3 -30
  35. data/lib/rack/contrib/signals.rb +6 -0
  36. data/lib/rack/contrib/simple_endpoint.rb +3 -1
  37. data/lib/rack/contrib/static_cache.rb +17 -8
  38. data/lib/rack/contrib/time_zone.rb +2 -0
  39. data/lib/rack/contrib/try_static.rb +2 -0
  40. metadata +50 -32
  41. data/lib/rack/contrib/accept_format.rb +0 -66
  42. data/lib/rack/contrib/sendfile.rb +0 -138
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bee6a82e9e3cd0238bfc695f9d18a74045235f86
4
- data.tar.gz: a340facd62eb27f47bbce5e5e02f15c949454946
2
+ SHA256:
3
+ metadata.gz: cf14e9cb70d9701bc1f3fe5ce036a0667f4ba6c9411e0e6ded73ef958228ec45
4
+ data.tar.gz: d0c2abe31908dd0d49dec3635cca17c720c94d098a04e9485adf917c469e0ee5
5
5
  SHA512:
6
- metadata.gz: 6f3bee839cd6955789228948379912610ac8c9ea8dd2cfd2339f43b2e15e311ee0aacd65f71304977c2fb78624cd374a1b8e071e9129da0db2cde9e61828b64a
7
- data.tar.gz: 5c0ce1dc50158e6790db66d758f7a9a16f6d5b0fa87a50f759e00c17d9a7ea5226ebc16e275f3f9e687306867d937bf1cadff224f763481f0991aa0650b58dc2
6
+ metadata.gz: f352f5eac5008e79d645460cd9b17dcc7231fe55edb63a0540c6c0991c06ba5f989cd70c23453e854a8189bcd02ad1be96366bb4d24efdaccefc5fe823a9eed5
7
+ data.tar.gz: 379d6d0190a63d1df9c615e2fdd53bfebcac0cf506407e1e8a11c734ffc955f066a2aa56e79db21458b62efbb6603e1749f1bdddaed1c35fb84a69cd494dba8f
data/README.md CHANGED
@@ -3,17 +3,16 @@
3
3
  This package includes a variety of add-on components for Rack, a Ruby web server
4
4
  interface:
5
5
 
6
- * `Rack::AcceptFormat` - Adds a format extension at the end of the URI when there is none, corresponding to the mime-type given in the Accept HTTP header.
7
6
  * `Rack::Access` - Limits access based on IP address
8
7
  * `Rack::Backstage` - Returns content of specified file if it exists, which makes it convenient for putting up maintenance pages.
9
8
  * `Rack::BounceFavicon` - Returns a 404 for requests to `/favicon.ico`
10
9
  * `Rack::CSSHTTPRequest` - Adds CSSHTTPRequest support by encoding responses as CSS for cross-site AJAX-style data loading
11
10
  * `Rack::Callbacks` - Implements DSL for pure before/after filter like Middlewares.
12
- * `Rack::Config` - Shared configuration for cooperative middleware.
13
11
  * `Rack::Cookies` - Adds simple cookie jar hash to env
14
12
  * `Rack::Deflect` - Helps protect against DoS attacks.
15
13
  * `Rack::Evil` - Lets the rack application return a response to the client from any place.
16
14
  * `Rack::HostMeta` - Configures `/host-meta` using a block
15
+ * `Rack::JSONBodyParser` - Adds JSON request bodies to the Rack parameters hash.
17
16
  * `Rack::JSONP` - Adds JSON-P support by stripping out the callback param and padding the response with the appropriate callback format.
18
17
  * `Rack::LazyConditionalGet` - Caches a global `Last-Modified` date and updates it each time there is a request that is not `GET` or `HEAD`.
19
18
  * `Rack::LighttpdScriptNameFix` - Fixes how lighttpd sets the `SCRIPT_NAME` and `PATH_INFO` variables in certain configurations.
@@ -21,14 +20,13 @@ interface:
21
20
  * `Rack::MailExceptions` - Rescues exceptions raised from the app and sends a useful email with the exception, stacktrace, and contents of the environment.
22
21
  * `Rack::NestedParams` - parses form params with subscripts (e.g., * "`post[title]=Hello`") into a nested/recursive Hash structure (based on Rails' implementation).
23
22
  * `Rack::NotFound` - A default 404 application.
24
- * `Rack::PostBodyContentTypeParser` - Adds support for JSON request bodies. The Rack parameter hash is populated by deserializing the JSON data provided in the request body when the Content-Type is application/json.
23
+ * `Rack::PostBodyContentTypeParser` - [Deprecated]: Adds support for JSON request bodies. The Rack parameter hash is populated by deserializing the JSON data provided in the request body when the Content-Type is application/json
25
24
  * `Rack::Printout` - Prints the environment and the response per request
26
25
  * `Rack::ProcTitle` - Displays request information in process title (`$0`) for monitoring/inspection with ps(1).
27
26
  * `Rack::Profiler` - Uses ruby-prof to measure request time.
28
27
  * `Rack::RelativeRedirect` - Transforms relative paths in redirects to absolute URLs.
29
- * `Rack::ResponseCache` - Caches responses to requests without query strings to Disk or a user provider Ruby object. Similar to Rails' page caching.
28
+ * `Rack::ResponseCache` - Caches responses to requests without query strings to Disk or a user provided Ruby object. Similar to Rails' page caching.
30
29
  * `Rack::ResponseHeaders` - Manipulates response headers object at runtime
31
- * `Rack::Sendfile` - Enables `X-Sendfile` support for bodies that can be served from file.
32
30
  * `Rack::Signals` - Installs signal handlers that are safely processed after a request
33
31
  * `Rack::SimpleEndpoint` - Creates simple endpoints with routing rules, similar to Sinatra actions
34
32
  * `Rack::StaticCache` - Modifies the response headers to facilitiate client and proxy caching for static files that minimizes http requests and improves overall load times for second time visitors.
@@ -61,6 +59,17 @@ use Rack::MailExceptions
61
59
  run theapp
62
60
  ```
63
61
 
62
+ #### Versioning
63
+
64
+ This package is [semver compliant](https://semver.org); you should use a
65
+ pessimistic version constraint (`~>`) against the relevant `2.x` version of
66
+ this gem.
67
+
68
+ This version of `rack-contrib` is only compatible with `rack` 2.x. If you
69
+ are using `rack` 1.x, you will need to use `rack-contrib` 1.x. A suitable
70
+ pessimistic version constraint (`~>`) is recommended.
71
+
72
+
64
73
  ### Testing
65
74
 
66
75
  To contribute to the project, begin by cloning the repo and installing the necessary gems:
@@ -93,8 +102,6 @@ guidelines in CONTRIBUTING.md.
93
102
 
94
103
  ### Links
95
104
 
96
- * rack-contrib on GitHub:: <http://github.com/rack/rack-contrib>
97
- * Rack:: <http://rack.rubyforge.org/>
98
- * Rack On GitHub:: <http://github.com/rack/rack>
99
- * rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
100
- * [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/rack/rack-contrib?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
105
+ * rack-contrib on GitHub:: <https://github.com/rack/rack-contrib>
106
+ * Rack:: <https://rack.github.io/>
107
+ * Rack On GitHub:: <https://github.com/rack/rack>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
 
3
5
  module Rack
@@ -14,7 +16,6 @@ module Rack
14
16
  end
15
17
  end
16
18
 
17
- autoload :AcceptFormat, "rack/contrib/accept_format"
18
19
  autoload :Access, "rack/contrib/access"
19
20
  autoload :BounceFavicon, "rack/contrib/bounce_favicon"
20
21
  autoload :Cookies, "rack/contrib/cookies"
@@ -25,6 +26,7 @@ module Rack
25
26
  autoload :HostMeta, "rack/contrib/host_meta"
26
27
  autoload :GarbageCollector, "rack/contrib/garbagecollector"
27
28
  autoload :JSONP, "rack/contrib/jsonp"
29
+ autoload :JSONBodyParser, "rack/contrib/json_body_parser"
28
30
  autoload :LazyConditionalGet, "rack/contrib/lazy_conditional_get"
29
31
  autoload :LighttpdScriptNameFix, "rack/contrib/lighttpd_script_name_fix"
30
32
  autoload :Locale, "rack/contrib/locale"
@@ -33,8 +35,6 @@ module Rack
33
35
  autoload :ProcTitle, "rack/contrib/proctitle"
34
36
  autoload :Profiler, "rack/contrib/profiler"
35
37
  autoload :ResponseHeaders, "rack/contrib/response_headers"
36
- autoload :Runtime, "rack/contrib/runtime"
37
- autoload :Sendfile, "rack/contrib/sendfile"
38
38
  autoload :Signals, "rack/contrib/signals"
39
39
  autoload :SimpleEndpoint, "rack/contrib/simple_endpoint"
40
40
  autoload :TimeZone, "rack/contrib/time_zone"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ipaddr"
2
4
 
3
5
  module Rack
@@ -48,9 +50,9 @@ module Rack
48
50
  end
49
51
 
50
52
  def call(env)
51
- @original_request = Request.new(env)
53
+ request = Request.new(env)
52
54
  ipmasks = ipmasks_for_path(env)
53
- return forbidden! unless ip_authorized?(ipmasks)
55
+ return forbidden! unless ip_authorized?(request, ipmasks)
54
56
  status, headers, body = @app.call(env)
55
57
  [status, headers, body]
56
58
  end
@@ -73,11 +75,11 @@ module Rack
73
75
  [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
74
76
  end
75
77
 
76
- def ip_authorized?(ipmasks)
78
+ def ip_authorized?(request, ipmasks)
77
79
  return true if ipmasks.nil?
78
80
 
79
81
  ipmasks.any? do |ip_mask|
80
- ip_mask.include?(IPAddr.new(@original_request.ip))
82
+ ip_mask.include?(IPAddr.new(request.ip))
81
83
  end
82
84
  end
83
85
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Backstage
3
5
  File = ::File
@@ -10,7 +12,7 @@ module Rack
10
12
  def call(env)
11
13
  if File.exists?(@file)
12
14
  content = File.read(@file)
13
- length = "".respond_to?(:bytesize) ? content.bytesize.to_s : content.size.to_s
15
+ length = content.bytesize.to_s
14
16
  [503, {'Content-Type' => 'text/html', 'Content-Length' => length}, [content]]
15
17
  else
16
18
  @app.call(env)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Bounce those annoying favicon.ico requests
3
5
  class BounceFavicon
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Callbacks
3
5
  def initialize(&block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack middleware to use common cookies across domain and subdomains.
3
5
  class CommonCookies
@@ -10,21 +12,24 @@ module Rack
10
12
  end
11
13
 
12
14
  def call(env)
13
- @app.call(env).tap do |(status, headers, response)|
14
- @host = env['HTTP_HOST'].sub PORT, ''
15
- share_cookie headers
16
- end
15
+ status, headers, body = @app.call(env)
16
+ headers = Utils::HeaderHash.new(headers)
17
+
18
+ host = env['HTTP_HOST'].sub PORT, ''
19
+ share_cookie(headers, host)
20
+
21
+ [status, headers, body]
17
22
  end
18
23
 
19
24
  private
20
25
 
21
- def domain
22
- @host =~ DOMAIN_REGEXP
26
+ def domain(host)
27
+ host =~ DOMAIN_REGEXP
23
28
  ".#{$1}.#{$2}"
24
29
  end
25
30
 
26
- def share_cookie(headers)
27
- headers['Set-Cookie'] &&= common_cookie(headers) if @host !~ LOCALHOST_OR_IP_REGEXP
31
+ def share_cookie(headers, host)
32
+ headers['Set-Cookie'] &&= common_cookie(headers, host) if host !~ LOCALHOST_OR_IP_REGEXP
28
33
  end
29
34
 
30
35
  def cookie(headers)
@@ -32,8 +37,8 @@ module Rack
32
37
  cookies.is_a?(Array) ? cookies.join("\n") : cookies
33
38
  end
34
39
 
35
- def common_cookie(headers)
36
- cookie(headers).gsub(/; domain=[^;]*/, '').gsub(/$/, "; domain=#{domain}")
40
+ def common_cookie(headers, host)
41
+ cookie(headers).gsub(/; domain=[^;]*/, '').gsub(/$/, "; domain=#{domain(host)}")
37
42
  end
38
43
  end
39
- end
44
+ end
@@ -1,16 +1,4 @@
1
- module Rack
1
+ # frozen_string_literal: true
2
2
 
3
- # Rack::Config modifies the environment using the block given during
4
- # initialization.
5
- class Config
6
- def initialize(app, &block)
7
- @app = app
8
- @block = block
9
- end
10
-
11
- def call(env)
12
- @block.call(env)
13
- @app.call(env)
14
- end
15
- end
16
- end
3
+ require 'rack'
4
+ require 'rack/config'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Cookies
3
5
  class CookieJar < Hash
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csshttprequest'
2
4
 
3
5
  module Rack
@@ -13,9 +15,12 @@ module Rack
13
15
  # the CSSHTTPRequest encoder
14
16
  def call(env)
15
17
  status, headers, response = @app.call(env)
18
+ headers = Utils::HeaderHash.new(headers)
19
+
16
20
  if chr_request?(env)
17
- response = encode(response)
18
- modify_headers!(headers, response)
21
+ encoded_response = encode(response)
22
+ modify_headers!(headers, encoded_response)
23
+ response = [encoded_response]
19
24
  end
20
25
  [status, headers, response]
21
26
  end
@@ -25,13 +30,12 @@ module Rack
25
30
  !(/\.chr$/.match(env['PATH_INFO'])).nil? || Rack::Request.new(env).params['_format'] == 'chr'
26
31
  end
27
32
 
28
- def encode(response, assembled_body="")
29
- response.each { |s| assembled_body << s.to_s } # call down the stack
30
- return ::CSSHTTPRequest.encode(assembled_body)
33
+ def encode(body)
34
+ ::CSSHTTPRequest.encode(body.to_enum.to_a.join)
31
35
  end
32
36
 
33
37
  def modify_headers!(headers, encoded_response)
34
- headers['Content-Length'] = encoded_response.length.to_s
38
+ headers['Content-Length'] = encoded_response.bytesize.to_s
35
39
  headers['Content-Type'] = 'text/css'
36
40
  nil
37
41
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  # TODO: optional stats
@@ -67,10 +69,10 @@ module Rack
67
69
  end
68
70
 
69
71
  def deflect? env
70
- @remote_addr = env['REMOTE_ADDR']
71
- return false if options[:whitelist].include? @remote_addr
72
- return true if options[:blacklist].include? @remote_addr
73
- sync { watch }
72
+ remote_addr = env['REMOTE_ADDR']
73
+ return false if options[:whitelist].include? remote_addr
74
+ return true if options[:blacklist].include? remote_addr
75
+ sync { watch(remote_addr) }
74
76
  end
75
77
 
76
78
  def log message
@@ -82,55 +84,55 @@ module Rack
82
84
  @mutex.synchronize(&block)
83
85
  end
84
86
 
85
- def map
86
- @remote_addr_map[@remote_addr] ||= {
87
+ def map(remote_addr)
88
+ @remote_addr_map[remote_addr] ||= {
87
89
  :expires => Time.now + options[:interval],
88
90
  :requests => 0
89
91
  }
90
92
  end
91
93
 
92
- def watch
93
- increment_requests
94
- clear! if watch_expired? and not blocked?
95
- clear! if blocked? and block_expired?
96
- block! if watching? and exceeded_request_threshold?
97
- blocked?
94
+ def watch(remote_addr)
95
+ increment_requests(remote_addr)
96
+ clear!(remote_addr) if watch_expired?(remote_addr) and not blocked?(remote_addr)
97
+ clear!(remote_addr) if blocked?(remote_addr) and block_expired?(remote_addr)
98
+ block!(remote_addr) if watching?(remote_addr) and exceeded_request_threshold?(remote_addr)
99
+ blocked?(remote_addr)
98
100
  end
99
101
 
100
- def block!
101
- return if blocked?
102
- log "blocked #{@remote_addr}"
103
- map[:block_expires] = Time.now + options[:block_duration]
102
+ def block!(remote_addr)
103
+ return if blocked?(remote_addr)
104
+ log "blocked #{remote_addr}"
105
+ map(remote_addr)[:block_expires] = Time.now + options[:block_duration]
104
106
  end
105
107
 
106
- def blocked?
107
- map.has_key? :block_expires
108
+ def blocked?(remote_addr)
109
+ map(remote_addr).has_key? :block_expires
108
110
  end
109
111
 
110
- def block_expired?
111
- map[:block_expires] < Time.now rescue false
112
+ def block_expired?(remote_addr)
113
+ map(remote_addr)[:block_expires] < Time.now rescue false
112
114
  end
113
115
 
114
- def watching?
115
- @remote_addr_map.has_key? @remote_addr
116
+ def watching?(remote_addr)
117
+ @remote_addr_map.has_key? remote_addr
116
118
  end
117
119
 
118
- def clear!
119
- return unless watching?
120
- log "released #{@remote_addr}" if blocked?
121
- @remote_addr_map.delete @remote_addr
120
+ def clear!(remote_addr)
121
+ return unless watching?(remote_addr)
122
+ log "released #{remote_addr}" if blocked?(remote_addr)
123
+ @remote_addr_map.delete remote_addr
122
124
  end
123
125
 
124
- def increment_requests
125
- map[:requests] += 1
126
+ def increment_requests(remote_addr)
127
+ map(remote_addr)[:requests] += 1
126
128
  end
127
129
 
128
- def exceeded_request_threshold?
129
- map[:requests] > options[:request_threshold]
130
+ def exceeded_request_threshold?(remote_addr)
131
+ map(remote_addr)[:requests] > options[:request_threshold]
130
132
  end
131
133
 
132
- def watch_expired?
133
- map[:expires] <= Time.now
134
+ def watch_expired?(remote_addr)
135
+ map(remote_addr)[:expires] <= Time.now
134
136
  end
135
137
 
136
138
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Ensure that the path and query string presented to the application
3
5
  # contains only valid characters. If the validation fails, then a
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Evil
3
5
  # Lets you return a response to the client immediately from anywhere ( M V or C ) in the code.
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class ExpectationCascade
3
- Expect = "Expect".freeze
5
+ Expect = "HTTP_EXPECT".freeze
4
6
  ContinueExpectation = "100-continue".freeze
5
7
 
6
8
  ExpectationFailed = [417, {"Content-Type" => "text/html"}, []].freeze
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Forces garbage collection after each request.
3
5
  class GarbageCollector
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
 
3
5
  # Rack middleware implementing the IETF draft: "Host Metadata for the Web"
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Rack
6
+ # A Rack middleware that makes JSON-encoded request bodies available in the
7
+ # request.params hash. By default it parses POST, PATCH, and PUT requests
8
+ # whose media type is <tt>application/json</tt>. You can configure it to match
9
+ # any verb or media type via the <tt>:verbs</tt> and <tt>:media</tt> options.
10
+ #
11
+ #
12
+ # == Examples:
13
+ #
14
+ # === Parse POST and GET requests only
15
+ # use Rack::JSONBodyParser, verbs: ['POST', 'GET']
16
+ #
17
+ # === Parse POST|PATCH|PUT requests whose Content-Type matches 'json'
18
+ # use Rack::JSONBodyParser, media: /json/
19
+ #
20
+ # === Parse POST requests whose Content-Type is 'application/json' or 'application/vnd+json'
21
+ # use Rack::JSONBodyParser, verbs: ['POST'], media: ['application/json', 'application/vnd.api+json']
22
+ #
23
+ class JSONBodyParser
24
+ CONTENT_TYPE_MATCHERS = {
25
+ String => lambda { |option, header|
26
+ Rack::MediaType.type(header) == option
27
+ },
28
+ Array => lambda { |options, header|
29
+ media_type = Rack::MediaType.type(header)
30
+ options.any? { |opt| media_type == opt }
31
+ },
32
+ Regexp => lambda {
33
+ if //.respond_to?(:match?)
34
+ # Use Ruby's fast regex matcher when available
35
+ ->(option, header) { option.match? header }
36
+ else
37
+ # Fall back to the slower matcher for rubies older than 2.4
38
+ ->(option, header) { option.match header }
39
+ end
40
+ }.call(),
41
+ }.freeze
42
+
43
+ DEFAULT_PARSER = ->(body) { JSON.parse(body, create_additions: false) }
44
+
45
+ def initialize(
46
+ app,
47
+ verbs: %w[POST PATCH PUT],
48
+ media: 'application/json',
49
+ &block
50
+ )
51
+ @app = app
52
+ @verbs, @media = verbs, media
53
+ @matcher = CONTENT_TYPE_MATCHERS.fetch(@media.class)
54
+ @parser = block || DEFAULT_PARSER
55
+ end
56
+
57
+ def call(env)
58
+ begin
59
+ if @verbs.include?(env[Rack::REQUEST_METHOD]) &&
60
+ @matcher.call(@media, env['CONTENT_TYPE'])
61
+
62
+ update_form_hash_with_json_body(env)
63
+ end
64
+ rescue JSON::ParserError
65
+ body = { error: 'Failed to parse body as JSON' }.to_json
66
+ header = { 'Content-Type' => 'application/json' }
67
+ return Rack::Response.new(body, 400, header).finish
68
+ end
69
+ @app.call(env)
70
+ end
71
+
72
+ private
73
+
74
+ def update_form_hash_with_json_body(env)
75
+ body = env[Rack::RACK_INPUT]
76
+ return unless (body_content = body.read) && !body_content.empty?
77
+
78
+ body.rewind # somebody might try to read this stream
79
+ env.update(
80
+ Rack::RACK_REQUEST_FORM_HASH => @parser.call(body_content),
81
+ Rack::RACK_REQUEST_FORM_INPUT => body
82
+ )
83
+ end
84
+ end
85
+ end