http 6.0.0-java

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/CONTRIBUTING.md +26 -0
  4. data/LICENSE.txt +20 -0
  5. data/README.md +263 -0
  6. data/SECURITY.md +17 -0
  7. data/UPGRADING.md +491 -0
  8. data/http.gemspec +48 -0
  9. data/lib/http/base64.rb +22 -0
  10. data/lib/http/chainable/helpers.rb +62 -0
  11. data/lib/http/chainable/verbs.rb +136 -0
  12. data/lib/http/chainable.rb +377 -0
  13. data/lib/http/client.rb +230 -0
  14. data/lib/http/connection/internals.rb +141 -0
  15. data/lib/http/connection.rb +265 -0
  16. data/lib/http/content_type.rb +89 -0
  17. data/lib/http/errors.rb +67 -0
  18. data/lib/http/feature.rb +86 -0
  19. data/lib/http/features/auto_deflate.rb +230 -0
  20. data/lib/http/features/auto_inflate.rb +64 -0
  21. data/lib/http/features/caching/entry.rb +178 -0
  22. data/lib/http/features/caching/in_memory_store.rb +63 -0
  23. data/lib/http/features/caching.rb +216 -0
  24. data/lib/http/features/digest_auth.rb +234 -0
  25. data/lib/http/features/instrumentation.rb +149 -0
  26. data/lib/http/features/logging.rb +231 -0
  27. data/lib/http/features/normalize_uri.rb +34 -0
  28. data/lib/http/features/raise_error.rb +37 -0
  29. data/lib/http/form_data/composite_io.rb +106 -0
  30. data/lib/http/form_data/file.rb +95 -0
  31. data/lib/http/form_data/multipart/param.rb +62 -0
  32. data/lib/http/form_data/multipart.rb +106 -0
  33. data/lib/http/form_data/part.rb +52 -0
  34. data/lib/http/form_data/readable.rb +58 -0
  35. data/lib/http/form_data/urlencoded.rb +175 -0
  36. data/lib/http/form_data/version.rb +8 -0
  37. data/lib/http/form_data.rb +102 -0
  38. data/lib/http/headers/known.rb +90 -0
  39. data/lib/http/headers/normalizer.rb +50 -0
  40. data/lib/http/headers.rb +343 -0
  41. data/lib/http/mime_type/adapter.rb +43 -0
  42. data/lib/http/mime_type/json.rb +41 -0
  43. data/lib/http/mime_type.rb +96 -0
  44. data/lib/http/options/definitions.rb +189 -0
  45. data/lib/http/options.rb +241 -0
  46. data/lib/http/redirector.rb +157 -0
  47. data/lib/http/request/body.rb +181 -0
  48. data/lib/http/request/builder.rb +184 -0
  49. data/lib/http/request/proxy.rb +83 -0
  50. data/lib/http/request/writer.rb +186 -0
  51. data/lib/http/request.rb +375 -0
  52. data/lib/http/response/body.rb +172 -0
  53. data/lib/http/response/inflater.rb +60 -0
  54. data/lib/http/response/parser.rb +223 -0
  55. data/lib/http/response/status/reasons.rb +79 -0
  56. data/lib/http/response/status.rb +263 -0
  57. data/lib/http/response.rb +350 -0
  58. data/lib/http/retriable/delay_calculator.rb +91 -0
  59. data/lib/http/retriable/errors.rb +35 -0
  60. data/lib/http/retriable/performer.rb +197 -0
  61. data/lib/http/session.rb +280 -0
  62. data/lib/http/timeout/global.rb +229 -0
  63. data/lib/http/timeout/null.rb +225 -0
  64. data/lib/http/timeout/per_operation.rb +197 -0
  65. data/lib/http/uri/normalizer.rb +82 -0
  66. data/lib/http/uri/parsing.rb +182 -0
  67. data/lib/http/uri.rb +376 -0
  68. data/lib/http/version.rb +6 -0
  69. data/lib/http.rb +36 -0
  70. data/sig/deps.rbs +122 -0
  71. data/sig/http.rbs +1619 -0
  72. data/test/http/base64_test.rb +28 -0
  73. data/test/http/client_test.rb +739 -0
  74. data/test/http/connection_test.rb +1533 -0
  75. data/test/http/content_type_test.rb +190 -0
  76. data/test/http/errors_test.rb +28 -0
  77. data/test/http/feature_test.rb +49 -0
  78. data/test/http/features/auto_deflate_test.rb +317 -0
  79. data/test/http/features/auto_inflate_test.rb +213 -0
  80. data/test/http/features/caching_test.rb +942 -0
  81. data/test/http/features/digest_auth_test.rb +996 -0
  82. data/test/http/features/instrumentation_test.rb +246 -0
  83. data/test/http/features/logging_test.rb +654 -0
  84. data/test/http/features/normalize_uri_test.rb +41 -0
  85. data/test/http/features/raise_error_test.rb +77 -0
  86. data/test/http/form_data/composite_io_test.rb +215 -0
  87. data/test/http/form_data/file_test.rb +255 -0
  88. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  89. data/test/http/form_data/multipart_test.rb +303 -0
  90. data/test/http/form_data/part_test.rb +90 -0
  91. data/test/http/form_data/urlencoded_test.rb +164 -0
  92. data/test/http/form_data_test.rb +232 -0
  93. data/test/http/headers/normalizer_test.rb +93 -0
  94. data/test/http/headers_test.rb +888 -0
  95. data/test/http/mime_type/json_test.rb +39 -0
  96. data/test/http/mime_type_test.rb +150 -0
  97. data/test/http/options/base_uri_test.rb +148 -0
  98. data/test/http/options/body_test.rb +21 -0
  99. data/test/http/options/features_test.rb +38 -0
  100. data/test/http/options/form_test.rb +21 -0
  101. data/test/http/options/headers_test.rb +32 -0
  102. data/test/http/options/json_test.rb +21 -0
  103. data/test/http/options/merge_test.rb +78 -0
  104. data/test/http/options/new_test.rb +37 -0
  105. data/test/http/options/proxy_test.rb +32 -0
  106. data/test/http/options_test.rb +575 -0
  107. data/test/http/redirector_test.rb +639 -0
  108. data/test/http/request/body_test.rb +318 -0
  109. data/test/http/request/builder_test.rb +623 -0
  110. data/test/http/request/writer_test.rb +391 -0
  111. data/test/http/request_test.rb +1733 -0
  112. data/test/http/response/body_test.rb +292 -0
  113. data/test/http/response/parser_test.rb +105 -0
  114. data/test/http/response/status_test.rb +322 -0
  115. data/test/http/response_test.rb +502 -0
  116. data/test/http/retriable/delay_calculator_test.rb +194 -0
  117. data/test/http/retriable/errors_test.rb +71 -0
  118. data/test/http/retriable/performer_test.rb +551 -0
  119. data/test/http/session_test.rb +424 -0
  120. data/test/http/timeout/global_test.rb +239 -0
  121. data/test/http/timeout/null_test.rb +218 -0
  122. data/test/http/timeout/per_operation_test.rb +220 -0
  123. data/test/http/uri/normalizer_test.rb +89 -0
  124. data/test/http/uri_test.rb +1140 -0
  125. data/test/http/version_test.rb +15 -0
  126. data/test/http_test.rb +818 -0
  127. data/test/regression_tests.rb +27 -0
  128. data/test/support/capture_warning.rb +10 -0
  129. data/test/support/dummy_server/encoding_routes.rb +47 -0
  130. data/test/support/dummy_server/routes.rb +201 -0
  131. data/test/support/dummy_server/servlet.rb +81 -0
  132. data/test/support/dummy_server.rb +200 -0
  133. data/test/support/fakeio.rb +21 -0
  134. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  135. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  136. data/test/support/http_handling_shared.rb +11 -0
  137. data/test/support/proxy_server.rb +207 -0
  138. data/test/support/servers/runner.rb +67 -0
  139. data/test/support/simplecov.rb +28 -0
  140. data/test/support/ssl_helper.rb +108 -0
  141. data/test/test_helper.rb +38 -0
  142. metadata +218 -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
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Headers
5
+ # Content-Types that are acceptable for the response.
6
+ ACCEPT = "Accept"
7
+
8
+ # Content-codings that are acceptable in the response.
9
+ ACCEPT_ENCODING = "Accept-Encoding"
10
+
11
+ # The age the object has been in a proxy cache in seconds.
12
+ AGE = "Age"
13
+
14
+ # Authentication credentials for HTTP authentication.
15
+ AUTHORIZATION = "Authorization"
16
+
17
+ # Used to specify directives that must be obeyed by all caching mechanisms
18
+ # along the request-response chain.
19
+ CACHE_CONTROL = "Cache-Control"
20
+
21
+ # An HTTP cookie previously sent by the server with Set-Cookie.
22
+ COOKIE = "Cookie"
23
+
24
+ # Control options for the current connection and list
25
+ # of hop-by-hop request fields.
26
+ CONNECTION = "Connection"
27
+
28
+ # The length of the request body in octets (8-bit bytes).
29
+ CONTENT_LENGTH = "Content-Length"
30
+
31
+ # The MIME type of the body of the request
32
+ # (used with POST and PUT requests).
33
+ CONTENT_TYPE = "Content-Type"
34
+
35
+ # The date and time that the message was sent (in "HTTP-date" format as
36
+ # defined by RFC 7231 Date/Time Formats).
37
+ DATE = "Date"
38
+
39
+ # An identifier for a specific version of a resource,
40
+ # often a message digest.
41
+ ETAG = "ETag"
42
+
43
+ # Gives the date/time after which the response is considered stale (in
44
+ # "HTTP-date" format as defined by RFC 7231).
45
+ EXPIRES = "Expires"
46
+
47
+ # The domain name of the server (for virtual hosting), and the TCP port
48
+ # number on which the server is listening. The port number may be omitted
49
+ # if the port is the standard port for the service requested.
50
+ HOST = "Host"
51
+
52
+ # Allows a 304 Not Modified to be returned if content is unchanged.
53
+ IF_MODIFIED_SINCE = "If-Modified-Since"
54
+
55
+ # Allows a 304 Not Modified to be returned if content is unchanged.
56
+ IF_NONE_MATCH = "If-None-Match"
57
+
58
+ # The last modified date for the requested object (in "HTTP-date" format as
59
+ # defined by RFC 7231).
60
+ LAST_MODIFIED = "Last-Modified"
61
+
62
+ # Used in redirection, or when a new resource has been created.
63
+ LOCATION = "Location"
64
+
65
+ # Authorization credentials for connecting to a proxy.
66
+ PROXY_AUTHORIZATION = "Proxy-Authorization"
67
+
68
+ # An HTTP cookie.
69
+ SET_COOKIE = "Set-Cookie"
70
+
71
+ # The form of encoding used to safely transfer the entity to the user.
72
+ # Currently defined methods are: chunked, compress, deflate, gzip, identity.
73
+ TRANSFER_ENCODING = "Transfer-Encoding"
74
+
75
+ # Chunked transfer coding value for Transfer-Encoding
76
+ CHUNKED = "chunked"
77
+
78
+ # Indicates what additional content codings have been applied to the
79
+ # entity-body.
80
+ CONTENT_ENCODING = "Content-Encoding"
81
+
82
+ # The user agent string of the user agent.
83
+ USER_AGENT = "User-Agent"
84
+
85
+ # Tells downstream proxies how to match future request headers to decide
86
+ # whether the cached response can be used rather than requesting a fresh
87
+ # one from the origin server.
88
+ VARY = "Vary"
89
+ end
90
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Headers
5
+ # Normalizes HTTP header names to canonical form
6
+ class Normalizer
7
+ # Matches valid header field name according to RFC.
8
+ # @see http://tools.ietf.org/html/rfc7230#section-3.2
9
+ COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
10
+
11
+ # Pattern matching header name part separators (hyphens and underscores)
12
+ NAME_PARTS_SEPARATOR_RE = /[-_]/
13
+
14
+ # Thread-local cache key for normalized header names
15
+ CACHE_KEY = :http_headers_normalizer_cache
16
+
17
+ # Normalizes a header name to canonical form
18
+ #
19
+ # @example
20
+ # normalizer.call("content-type")
21
+ #
22
+ # @return [String]
23
+ # @api public
24
+ def call(name)
25
+ name = name.to_s
26
+ cache = (Thread.current[CACHE_KEY] ||= {})
27
+ value = (cache[name] ||= normalize_header(name))
28
+
29
+ value.dup
30
+ end
31
+
32
+ private
33
+
34
+ # Transforms name to canonical HTTP header capitalization
35
+ #
36
+ # @param [String] name
37
+ # @raise [HeaderError] if normalized name does not
38
+ # match {COMPLIANT_NAME_RE}
39
+ # @return [String] canonical HTTP header name
40
+ # @api private
41
+ def normalize_header(name)
42
+ normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-")
43
+
44
+ return normalized if COMPLIANT_NAME_RE.match?(normalized)
45
+
46
+ raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
47
+ end
48
+ end
49
+ end
50
+ end