http 5.3.1 → 6.0.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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /data/{spec → test}/support/capture_warning.rb +0 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ require "http/form_data/readable"
6
+
7
+ module HTTP
8
+ module FormData
9
+ # Represents a body part of multipart/form-data request.
10
+ #
11
+ # @example Usage with String
12
+ #
13
+ # body = "Message"
14
+ # FormData::Part.new body, content_type: 'foobar.txt; charset="UTF-8"'
15
+ class Part
16
+ include Readable
17
+
18
+ # Returns the content type of this part
19
+ #
20
+ # @example
21
+ # part.content_type # => "application/json"
22
+ #
23
+ # @api public
24
+ # @return [String, nil]
25
+ attr_reader :content_type
26
+
27
+ # Returns the filename of this part
28
+ #
29
+ # @example
30
+ # part.filename # => "avatar.png"
31
+ #
32
+ # @api public
33
+ # @return [String, nil]
34
+ attr_reader :filename
35
+
36
+ # Creates a new Part with the given body and options
37
+ #
38
+ # @example
39
+ # Part.new("hello", content_type: "text/plain")
40
+ #
41
+ # @api public
42
+ # @param [#to_s] body
43
+ # @param [String] content_type Value of Content-Type header
44
+ # @param [String] filename Value of filename parameter
45
+ def initialize(body, content_type: nil, filename: nil)
46
+ @io = StringIO.new(body.to_s)
47
+ @content_type = content_type
48
+ @filename = filename
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module FormData
5
+ # Common behaviour for objects defined by an IO object.
6
+ module Readable
7
+ # Returns IO content as a String
8
+ #
9
+ # @example
10
+ # readable.to_s # => "content"
11
+ #
12
+ # @api public
13
+ # @return [String]
14
+ def to_s
15
+ rewind
16
+ content = read #: String
17
+ rewind
18
+ content
19
+ end
20
+
21
+ # Reads and returns part of IO content
22
+ #
23
+ # @example
24
+ # readable.read # => "full content"
25
+ # readable.read(5) # => "full "
26
+ #
27
+ # @api public
28
+ # @param [Integer] length Number of bytes to retrieve
29
+ # @param [String] outbuf String to be replaced with retrieved data
30
+ # @return [String, nil]
31
+ def read(length = nil, outbuf = nil)
32
+ @io.read(length, outbuf)
33
+ end
34
+
35
+ # Returns IO size in bytes
36
+ #
37
+ # @example
38
+ # readable.size # => 42
39
+ #
40
+ # @api public
41
+ # @return [Integer]
42
+ def size
43
+ @io.size
44
+ end
45
+
46
+ # Rewinds the IO to the beginning
47
+ #
48
+ # @example
49
+ # readable.rewind
50
+ #
51
+ # @api public
52
+ # @return [void]
53
+ def rewind
54
+ @io.rewind
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/form_data/readable"
4
+
5
+ require "uri"
6
+ require "stringio"
7
+
8
+ module HTTP
9
+ module FormData
10
+ # `application/x-www-form-urlencoded` form data.
11
+ class Urlencoded
12
+ include Readable
13
+
14
+ class << self
15
+ # Sets custom form data encoder implementation
16
+ #
17
+ # @example
18
+ #
19
+ # module CustomFormDataEncoder
20
+ # UNESCAPED_CHARS = /[^a-z0-9\-\.\_\~]/i
21
+ #
22
+ # def self.escape(s)
23
+ # ::URI::DEFAULT_PARSER.escape(s.to_s, UNESCAPED_CHARS)
24
+ # end
25
+ #
26
+ # def self.call(data)
27
+ # parts = []
28
+ #
29
+ # data.each do |k, v|
30
+ # k = escape(k)
31
+ #
32
+ # if v.nil?
33
+ # parts << k
34
+ # elsif v.respond_to?(:to_ary)
35
+ # v.to_ary.each { |vv| parts << "#{k}=#{escape vv}" }
36
+ # else
37
+ # parts << "#{k}=#{escape v}"
38
+ # end
39
+ # end
40
+ #
41
+ # parts.join("&")
42
+ # end
43
+ # end
44
+ #
45
+ # HTTP::FormData::Urlencoded.encoder = CustomFormDataEncoder
46
+ #
47
+ # @api public
48
+ # @raise [ArgumentError] if implementation does not respond to `#call`
49
+ # @param implementation [#call]
50
+ # @return [void]
51
+ def encoder=(implementation)
52
+ raise ArgumentError unless implementation.respond_to? :call
53
+
54
+ @encoder = implementation
55
+ end
56
+
57
+ # Returns form data encoder implementation
58
+ #
59
+ # @example
60
+ # Urlencoded.encoder # => #<Method: DefaultEncoder.encode>
61
+ #
62
+ # @api public
63
+ # @see .encoder=
64
+ # @return [#call]
65
+ def encoder
66
+ @encoder || DefaultEncoder
67
+ end
68
+
69
+ # Default encoder for urlencoded form data
70
+ module DefaultEncoder
71
+ class << self
72
+ # Recursively encodes form data value
73
+ #
74
+ # @example
75
+ # DefaultEncoder.encode({ foo: "bar" }) # => "foo=bar"
76
+ #
77
+ # @api public
78
+ # @param [Hash, Array, String, nil] value
79
+ # @param [String, nil] prefix
80
+ # @return [String]
81
+ def encode(value, prefix = nil)
82
+ case value
83
+ when Hash then encode_hash(value, prefix)
84
+ when Array then encode_array(value, prefix)
85
+ when nil then prefix.to_s
86
+ else
87
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
88
+
89
+ "#{prefix}=#{escape(value)}"
90
+ end
91
+ end
92
+
93
+ alias call encode
94
+
95
+ private
96
+
97
+ # Encodes an Array value
98
+ #
99
+ # @api private
100
+ # @return [String]
101
+ def encode_array(value, prefix)
102
+ if prefix
103
+ value.map { |v| encode(v, "#{prefix}[]") }.join("&")
104
+ else
105
+ encode_pairs(value)
106
+ end
107
+ end
108
+
109
+ # Encodes an Array of key-value pairs
110
+ #
111
+ # @api private
112
+ # @return [String]
113
+ def encode_pairs(pairs)
114
+ pairs.map { |k, v| encode(v, escape(k)) }.reject(&:empty?).join("&")
115
+ end
116
+
117
+ # Encodes a Hash value
118
+ #
119
+ # @api private
120
+ # @return [String]
121
+ def encode_hash(hash, prefix)
122
+ hash.map do |k, v|
123
+ encode(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
124
+ end.reject(&:empty?).join("&")
125
+ end
126
+
127
+ # URL-encodes a value
128
+ #
129
+ # @api private
130
+ # @return [String]
131
+ def escape(value)
132
+ ::URI.encode_www_form_component(value)
133
+ end
134
+ end
135
+ end
136
+
137
+ private_constant :DefaultEncoder
138
+ end
139
+
140
+ # Creates a new Urlencoded form data instance
141
+ #
142
+ # @example
143
+ # Urlencoded.new({ "foo" => "bar" })
144
+ #
145
+ # @api public
146
+ # @param [Enumerable, Hash, #to_h] data form data key-value pairs
147
+ # @param [#call] encoder custom encoder implementation
148
+ def initialize(data, encoder: nil)
149
+ encoder ||= self.class.encoder
150
+ @io = StringIO.new(encoder.call(FormData.ensure_data(data)))
151
+ end
152
+
153
+ # Returns MIME type for the Content-Type header
154
+ #
155
+ # @example
156
+ # urlencoded.content_type
157
+ # # => "application/x-www-form-urlencoded"
158
+ #
159
+ # @api public
160
+ # @return [String]
161
+ def content_type
162
+ "application/x-www-form-urlencoded"
163
+ end
164
+
165
+ # Returns form data content size for Content-Length
166
+ #
167
+ # @example
168
+ # urlencoded.content_length # => 17
169
+ #
170
+ # @api public
171
+ # @return [Integer]
172
+ alias content_length size
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module FormData
5
+ # Gem version.
6
+ VERSION = "3.0.0"
7
+ end
8
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/form_data/part"
4
+ require "http/form_data/file"
5
+ require "http/form_data/multipart"
6
+ require "http/form_data/urlencoded"
7
+ require "http/form_data/version"
8
+
9
+ # http gem namespace.
10
+ # @see https://github.com/httprb/http
11
+ module HTTP
12
+ # Utility-belt to build form data request bodies.
13
+ # Provides support for `application/x-www-form-urlencoded` and
14
+ # `multipart/form-data` types.
15
+ #
16
+ # @example Usage
17
+ #
18
+ # form = FormData.create({
19
+ # username: "ixti",
20
+ # avatar_file: FormData::File.new("/home/ixti/avatar.png")
21
+ # })
22
+ #
23
+ # # Assuming socket is an open socket to some HTTP server
24
+ # socket << "POST /some-url HTTP/1.1\r\n"
25
+ # socket << "Host: example.com\r\n"
26
+ # socket << "Content-Type: #{form.content_type}\r\n"
27
+ # socket << "Content-Length: #{form.content_length}\r\n"
28
+ # socket << "\r\n"
29
+ # socket << form.to_s
30
+ module FormData
31
+ # CRLF
32
+ CRLF = "\r\n"
33
+
34
+ # Generic FormData error.
35
+ class Error < StandardError; end
36
+
37
+ class << self
38
+ # Selects encoder type based on given data
39
+ #
40
+ # @example
41
+ # FormData.create({ username: "ixti" })
42
+ #
43
+ # @api public
44
+ # @param [Enumerable, Hash, #to_h] data
45
+ # @return [Multipart] if any of values is a {FormData::File}
46
+ # @return [Urlencoded] otherwise
47
+ def create(data, encoder: nil)
48
+ data = ensure_data data
49
+
50
+ if multipart?(data)
51
+ Multipart.new(data)
52
+ else
53
+ Urlencoded.new(data, encoder: encoder)
54
+ end
55
+ end
56
+
57
+ # Coerces obj to Hash
58
+ #
59
+ # @example
60
+ # FormData.ensure_hash({ foo: :bar }) # => { foo: :bar }
61
+ #
62
+ # @api public
63
+ # @raise [Error] `obj` can't be coerced
64
+ # @return [Hash]
65
+ def ensure_hash(obj)
66
+ if obj.is_a?(Hash) then obj
67
+ elsif obj.respond_to?(:to_h) then obj.to_h
68
+ else raise Error, "#{obj.inspect} is neither Hash nor responds to :to_h"
69
+ end
70
+ end
71
+
72
+ # Coerces obj to an Enumerable of key-value pairs
73
+ #
74
+ # @example
75
+ # FormData.ensure_data([[:foo, :bar]]) # => [[:foo, :bar]]
76
+ #
77
+ # @api public
78
+ # @raise [Error] `obj` can't be coerced
79
+ # @return [Enumerable]
80
+ def ensure_data(obj)
81
+ if obj.nil? then []
82
+ elsif obj.is_a?(Enumerable) then obj
83
+ elsif obj.respond_to?(:to_h) then obj.to_h
84
+ else raise Error, "#{obj.inspect} is neither Enumerable nor responds to :to_h"
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # Checks if data contains multipart data
91
+ #
92
+ # @api private
93
+ # @param [Enumerable] data
94
+ # @return [Boolean]
95
+ def multipart?(data)
96
+ data.any? do |_, v|
97
+ v.is_a?(Part) || (v.respond_to?(:to_ary) && v.to_ary.any?(Part))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -72,6 +72,9 @@ module HTTP
72
72
  # Currently defined methods are: chunked, compress, deflate, gzip, identity.
73
73
  TRANSFER_ENCODING = "Transfer-Encoding"
74
74
 
75
+ # Chunked transfer coding value for Transfer-Encoding
76
+ CHUNKED = "chunked"
77
+
75
78
  # Indicates what additional content codings have been applied to the
76
79
  # entity-body.
77
80
  CONTENT_ENCODING = "Content-Encoding"
@@ -2,62 +2,43 @@
2
2
 
3
3
  module HTTP
4
4
  class Headers
5
+ # Normalizes HTTP header names to canonical form
5
6
  class Normalizer
6
- # Matches HTTP header names when in "Canonical-Http-Format"
7
- CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/
8
-
9
7
  # Matches valid header field name according to RFC.
10
8
  # @see http://tools.ietf.org/html/rfc7230#section-3.2
11
9
  COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
12
10
 
13
- NAME_PARTS_SEPARATOR_RE = /[\-_]/
14
-
15
- # @private
16
- # Normalized header names cache
17
- class Cache
18
- MAX_SIZE = 200
19
-
20
- def initialize
21
- @store = {}
22
- end
11
+ # Pattern matching header name part separators (hyphens and underscores)
12
+ NAME_PARTS_SEPARATOR_RE = /[-_]/
23
13
 
24
- def get(key)
25
- @store[key]
26
- end
27
- alias [] get
28
-
29
- def set(key, value)
30
- # Maintain cache size
31
- @store.delete(@store.each_key.first) while MAX_SIZE <= @store.size
32
-
33
- @store[key] = value
34
- end
35
- alias []= set
36
- end
14
+ # Thread-local cache key for normalized header names
15
+ CACHE_KEY = :http_headers_normalizer_cache
37
16
 
38
- def initialize
39
- @cache = Cache.new
40
- end
41
-
42
- # Transforms `name` to canonical HTTP header capitalization
17
+ # Normalizes a header name to canonical form
18
+ #
19
+ # @example
20
+ # normalizer.call("content-type")
21
+ #
22
+ # @return [String]
23
+ # @api public
43
24
  def call(name)
44
- name = -name.to_s
45
- value = (@cache[name] ||= -normalize_header(name))
25
+ name = name.to_s
26
+ cache = (Thread.current[CACHE_KEY] ||= {})
27
+ value = (cache[name] ||= normalize_header(name))
46
28
 
47
29
  value.dup
48
30
  end
49
31
 
50
32
  private
51
33
 
52
- # Transforms `name` to canonical HTTP header capitalization
34
+ # Transforms name to canonical HTTP header capitalization
53
35
  #
54
36
  # @param [String] name
55
37
  # @raise [HeaderError] if normalized name does not
56
38
  # match {COMPLIANT_NAME_RE}
57
39
  # @return [String] canonical HTTP header name
40
+ # @api private
58
41
  def normalize_header(name)
59
- return name if CANONICAL_NAME_RE.match?(name)
60
-
61
42
  normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-")
62
43
 
63
44
  return normalized if COMPLIANT_NAME_RE.match?(normalized)