rack-contrib 1.8.0 → 2.3.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.

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