http 3.1.0 → 5.3.1

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 (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
data/lib/http/feature.rb CHANGED
@@ -2,8 +2,25 @@
2
2
 
3
3
  module HTTP
4
4
  class Feature
5
- def initialize(opts = {}) # rubocop:disable Style/OptionHash
5
+ def initialize(opts = {})
6
6
  @opts = opts
7
7
  end
8
+
9
+ def wrap_request(request)
10
+ request
11
+ end
12
+
13
+ def wrap_response(response)
14
+ response
15
+ end
16
+
17
+ def on_error(request, error); end
8
18
  end
9
19
  end
20
+
21
+ require "http/features/auto_inflate"
22
+ require "http/features/auto_deflate"
23
+ require "http/features/instrumentation"
24
+ require "http/features/logging"
25
+ require "http/features/normalize_uri"
26
+ require "http/features/raise_error"
@@ -3,12 +3,14 @@
3
3
  require "zlib"
4
4
  require "tempfile"
5
5
 
6
+ require "http/request/body"
7
+
6
8
  module HTTP
7
9
  module Features
8
10
  class AutoDeflate < Feature
9
11
  attr_reader :method
10
12
 
11
- def initialize(*)
13
+ def initialize(**)
12
14
  super
13
15
 
14
16
  @method = @opts.key?(:method) ? @opts[:method].to_s : "gzip"
@@ -16,20 +18,39 @@ module HTTP
16
18
  raise Error, "Only gzip and deflate methods are supported" unless %w[gzip deflate].include?(@method)
17
19
  end
18
20
 
21
+ def wrap_request(request)
22
+ return request unless method
23
+ return request if request.body.size.zero?
24
+
25
+ # We need to delete Content-Length header. It will be set automatically by HTTP::Request::Writer
26
+ request.headers.delete(Headers::CONTENT_LENGTH)
27
+ request.headers[Headers::CONTENT_ENCODING] = method
28
+
29
+ Request.new(
30
+ :version => request.version,
31
+ :verb => request.verb,
32
+ :uri => request.uri,
33
+ :headers => request.headers,
34
+ :proxy => request.proxy,
35
+ :body => deflated_body(request.body),
36
+ :uri_normalizer => request.uri_normalizer
37
+ )
38
+ end
39
+
19
40
  def deflated_body(body)
20
41
  case method
21
42
  when "gzip"
22
43
  GzippedBody.new(body)
23
44
  when "deflate"
24
45
  DeflatedBody.new(body)
25
- else
26
- raise ArgumentError, "Unsupported deflate method: #{method}"
27
46
  end
28
47
  end
29
48
 
30
- class CompressedBody
31
- def initialize(body)
32
- @body = body
49
+ HTTP::Options.register_feature(:auto_deflate, self)
50
+
51
+ class CompressedBody < HTTP::Request::Body
52
+ def initialize(uncompressed_body)
53
+ @body = uncompressed_body
33
54
  @compressed = nil
34
55
  end
35
56
 
@@ -1,15 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module HTTP
4
6
  module Features
5
7
  class AutoInflate < Feature
6
- def stream_for(connection, response)
7
- if %w[deflate gzip x-gzip].include?(response.headers[:content_encoding])
8
- Response::Inflater.new(connection)
9
- else
10
- connection
11
- end
8
+ SUPPORTED_ENCODING = Set.new(%w[deflate gzip x-gzip]).freeze
9
+ private_constant :SUPPORTED_ENCODING
10
+
11
+ def wrap_response(response)
12
+ return response unless supported_encoding?(response)
13
+
14
+ options = {
15
+ :status => response.status,
16
+ :version => response.version,
17
+ :headers => response.headers,
18
+ :proxy_headers => response.proxy_headers,
19
+ :connection => response.connection,
20
+ :body => stream_for(response.connection),
21
+ :request => response.request
22
+ }
23
+
24
+ Response.new(options)
25
+ end
26
+
27
+ def stream_for(connection)
28
+ Response::Body.new(Response::Inflater.new(connection))
12
29
  end
30
+
31
+ private
32
+
33
+ def supported_encoding?(response)
34
+ content_encoding = response.headers.get(Headers::CONTENT_ENCODING).first
35
+ content_encoding && SUPPORTED_ENCODING.include?(content_encoding)
36
+ end
37
+
38
+ HTTP::Options.register_feature(:auto_inflate, self)
13
39
  end
14
40
  end
15
41
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Features
5
+ # Instrument requests and responses. Expects an
6
+ # ActiveSupport::Notifications-compatible instrumenter. Defaults to use a
7
+ # namespace of 'http' which may be overridden with a `:namespace` param.
8
+ # Emits a single event like `"request.{namespace}"`, eg `"request.http"`.
9
+ # Be sure to specify the instrumenter when enabling the feature:
10
+ #
11
+ # HTTP
12
+ # .use(instrumentation: {instrumenter: ActiveSupport::Notifications.instrumenter})
13
+ # .get("https://example.com/")
14
+ #
15
+ # Emits two events on every request:
16
+ #
17
+ # * `start_request.http` before the request is made, so you can log the reqest being started
18
+ # * `request.http` after the response is recieved, and contains `start`
19
+ # and `finish` so the duration of the request can be calculated.
20
+ #
21
+ class Instrumentation < Feature
22
+ attr_reader :instrumenter, :name, :error_name
23
+
24
+ def initialize(instrumenter: NullInstrumenter.new, namespace: "http")
25
+ @instrumenter = instrumenter
26
+ @name = "request.#{namespace}"
27
+ @error_name = "error.#{namespace}"
28
+ end
29
+
30
+ def wrap_request(request)
31
+ # Emit a separate "start" event, so a logger can print the request
32
+ # being run without waiting for a response
33
+ instrumenter.instrument("start_#{name}", :request => request)
34
+ instrumenter.start(name, :request => request)
35
+ request
36
+ end
37
+
38
+ def wrap_response(response)
39
+ instrumenter.finish(name, :response => response)
40
+ response
41
+ end
42
+
43
+ def on_error(request, error)
44
+ instrumenter.instrument(error_name, :request => request, :error => error)
45
+ end
46
+
47
+ HTTP::Options.register_feature(:instrumentation, self)
48
+
49
+ class NullInstrumenter
50
+ def instrument(name, payload = {})
51
+ start(name, payload)
52
+ begin
53
+ yield payload if block_given?
54
+ ensure
55
+ finish name, payload
56
+ end
57
+ end
58
+
59
+ def start(_name, _payload)
60
+ true
61
+ end
62
+
63
+ def finish(_name, _payload)
64
+ true
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Features
5
+ # Log requests and responses. Request verb and uri, and Response status are
6
+ # logged at `info`, and the headers and bodies of both are logged at
7
+ # `debug`. Be sure to specify the logger when enabling the feature:
8
+ #
9
+ # HTTP.use(logging: {logger: Logger.new(STDOUT)}).get("https://example.com/")
10
+ #
11
+ class Logging < Feature
12
+ HTTP::Options.register_feature(:logging, self)
13
+
14
+ class NullLogger
15
+ %w[fatal error warn info debug].each do |level|
16
+ define_method(level.to_sym) do |*_args|
17
+ nil
18
+ end
19
+
20
+ define_method(:"#{level}?") do
21
+ true
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :logger
27
+
28
+ def initialize(logger: NullLogger.new)
29
+ @logger = logger
30
+ end
31
+
32
+ def wrap_request(request)
33
+ logger.info { "> #{request.verb.to_s.upcase} #{request.uri}" }
34
+ logger.debug { "#{stringify_headers(request.headers)}\n\n#{request.body.source}" }
35
+
36
+ request
37
+ end
38
+
39
+ def wrap_response(response)
40
+ logger.info { "< #{response.status}" }
41
+ logger.debug { "#{stringify_headers(response.headers)}\n\n#{response.body}" }
42
+
43
+ response
44
+ end
45
+
46
+ private
47
+
48
+ def stringify_headers(headers)
49
+ headers.map { |name, value| "#{name}: #{value}" }.join("\n")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/uri"
4
+
5
+ module HTTP
6
+ module Features
7
+ class NormalizeUri < Feature
8
+ attr_reader :normalizer
9
+
10
+ def initialize(normalizer: HTTP::URI::NORMALIZER)
11
+ @normalizer = normalizer
12
+ end
13
+
14
+ HTTP::Options.register_feature(:normalize_uri, self)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Features
5
+ class RaiseError < Feature
6
+ def initialize(ignore: [])
7
+ super()
8
+
9
+ @ignore = ignore
10
+ end
11
+
12
+ def wrap_response(response)
13
+ return response if response.code < 400
14
+ return response if @ignore.include?(response.code)
15
+
16
+ raise HTTP::StatusError, response
17
+ end
18
+
19
+ HTTP::Options.register_feature(:raise_error, self)
20
+ end
21
+ end
22
+ end
@@ -5,6 +5,9 @@ module HTTP
5
5
  # Content-Types that are acceptable for the response.
6
6
  ACCEPT = "Accept"
7
7
 
8
+ # Content-codings that are acceptable in the response.
9
+ ACCEPT_ENCODING = "Accept-Encoding"
10
+
8
11
  # The age the object has been in a proxy cache in seconds.
9
12
  AGE = "Age"
10
13
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Headers
5
+ 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
+ # Matches valid header field name according to RFC.
10
+ # @see http://tools.ietf.org/html/rfc7230#section-3.2
11
+ COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
12
+
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
23
+
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
37
+
38
+ def initialize
39
+ @cache = Cache.new
40
+ end
41
+
42
+ # Transforms `name` to canonical HTTP header capitalization
43
+ def call(name)
44
+ name = -name.to_s
45
+ value = (@cache[name] ||= -normalize_header(name))
46
+
47
+ value.dup
48
+ end
49
+
50
+ private
51
+
52
+ # Transforms `name` to canonical HTTP header capitalization
53
+ #
54
+ # @param [String] name
55
+ # @raise [HeaderError] if normalized name does not
56
+ # match {COMPLIANT_NAME_RE}
57
+ # @return [String] canonical HTTP header name
58
+ def normalize_header(name)
59
+ return name if CANONICAL_NAME_RE.match?(name)
60
+
61
+ normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-")
62
+
63
+ return normalized if COMPLIANT_NAME_RE.match?(normalized)
64
+
65
+ raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/http/headers.rb CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
4
4
 
5
5
  require "http/errors"
6
6
  require "http/headers/mixin"
7
+ require "http/headers/normalizer"
7
8
  require "http/headers/known"
8
9
 
9
10
  module HTTP
@@ -12,15 +13,38 @@ module HTTP
12
13
  extend Forwardable
13
14
  include Enumerable
14
15
 
15
- # Matches HTTP header names when in "Canonical-Http-Format"
16
- CANONICAL_NAME_RE = /^[A-Z][a-z]*(?:-[A-Z][a-z]*)*$/
16
+ class << self
17
+ # Coerces given `object` into Headers.
18
+ #
19
+ # @raise [Error] if object can't be coerced
20
+ # @param [#to_hash, #to_h, #to_a] object
21
+ # @return [Headers]
22
+ def coerce(object)
23
+ unless object.is_a? self
24
+ object = case
25
+ when object.respond_to?(:to_hash) then object.to_hash
26
+ when object.respond_to?(:to_h) then object.to_h
27
+ when object.respond_to?(:to_a) then object.to_a
28
+ else raise Error, "Can't coerce #{object.inspect} to Headers"
29
+ end
30
+ end
31
+ headers = new
32
+ object.each { |k, v| headers.add k, v }
33
+ headers
34
+ end
35
+ alias [] coerce
17
36
 
18
- # Matches valid header field name according to RFC.
19
- # @see http://tools.ietf.org/html/rfc7230#section-3.2
20
- COMPLIANT_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/
37
+ def normalizer
38
+ @normalizer ||= Headers::Normalizer.new
39
+ end
40
+ end
21
41
 
22
42
  # Class constructor.
23
43
  def initialize
44
+ # The @pile stores each header value using a three element array:
45
+ # 0 - the normalized header key, used for lookup
46
+ # 1 - the header key as it will be sent with a request
47
+ # 2 - the value
24
48
  @pile = []
25
49
  end
26
50
 
@@ -45,12 +69,31 @@ module HTTP
45
69
 
46
70
  # Appends header.
47
71
  #
48
- # @param [#to_s] name header name
72
+ # @param [String, Symbol] name header name. When specified as a string, the
73
+ # name is sent as-is. When specified as a symbol, the name is converted
74
+ # to a string of capitalized words separated by a dash. Word boundaries
75
+ # are determined by an underscore (`_`) or a dash (`-`).
76
+ # Ex: `:content_type` is sent as `"Content-Type"`, and `"auth_key"` (string)
77
+ # is sent as `"auth_key"`.
49
78
  # @param [Array<#to_s>, #to_s] value header value(s) to be appended
50
79
  # @return [void]
51
80
  def add(name, value)
52
- name = normalize_header name.to_s
53
- Array(value).each { |v| @pile << [name, v.to_s] }
81
+ lookup_name = normalize_header(name.to_s)
82
+ wire_name = case name
83
+ when String
84
+ name
85
+ when Symbol
86
+ lookup_name
87
+ else
88
+ raise HTTP::HeaderError, "HTTP header must be a String or Symbol: #{name.inspect}"
89
+ end
90
+ Array(value).each do |v|
91
+ @pile << [
92
+ lookup_name,
93
+ wire_name,
94
+ validate_value(v)
95
+ ]
96
+ end
54
97
  end
55
98
 
56
99
  # Returns list of header values if any.
@@ -58,7 +101,7 @@ module HTTP
58
101
  # @return [Array<String>]
59
102
  def get(name)
60
103
  name = normalize_header name.to_s
61
- @pile.select { |k, _| k == name }.map { |_, v| v }
104
+ @pile.select { |k, _| k == name }.map { |_, _, v| v }
62
105
  end
63
106
 
64
107
  # Smart version of {#get}.
@@ -88,7 +131,7 @@ module HTTP
88
131
  #
89
132
  # @return [Hash]
90
133
  def to_h
91
- Hash[keys.map { |k| [k, self[k]] }]
134
+ keys.to_h { |k| [k, self[k]] }
92
135
  end
93
136
  alias to_hash to_h
94
137
 
@@ -96,7 +139,7 @@ module HTTP
96
139
  #
97
140
  # @return [Array<[String, String]>]
98
141
  def to_a
99
- @pile.map { |pair| pair.map(&:dup) }
142
+ @pile.map { |item| item[1..2] }
100
143
  end
101
144
 
102
145
  # Returns human-readable representation of `self` instance.
@@ -110,7 +153,7 @@ module HTTP
110
153
  #
111
154
  # @return [Array<String>]
112
155
  def keys
113
- @pile.map { |k, _| k }.uniq
156
+ @pile.map { |_, k, _| k }.uniq
114
157
  end
115
158
 
116
159
  # Compares headers to another Headers or Array of key/value pairs
@@ -118,7 +161,8 @@ module HTTP
118
161
  # @return [Boolean]
119
162
  def ==(other)
120
163
  return false unless other.respond_to? :to_a
121
- @pile == other.to_a
164
+
165
+ to_a == other.to_a
122
166
  end
123
167
 
124
168
  # Calls the given block once for each key/value pair in headers container.
@@ -127,7 +171,8 @@ module HTTP
127
171
  # @return [Headers] self-reference
128
172
  def each
129
173
  return to_enum(__method__) unless block_given?
130
- @pile.each { |arr| yield(arr) }
174
+
175
+ @pile.each { |item| yield(item[1..2]) }
131
176
  self
132
177
  end
133
178
 
@@ -139,7 +184,7 @@ module HTTP
139
184
 
140
185
  # @!method hash
141
186
  # Compute a hash-code for this headers container.
142
- # Two conatiners with the same content will have the same hash code.
187
+ # Two containers with the same content will have the same hash code.
143
188
  #
144
189
  # @see http://www.ruby-doc.org/core/Object.html#method-i-hash
145
190
  # @return [Fixnum]
@@ -150,7 +195,7 @@ module HTTP
150
195
  # @api private
151
196
  def initialize_copy(orig)
152
197
  super
153
- @pile = to_a
198
+ @pile = @pile.map(&:dup)
154
199
  end
155
200
 
156
201
  # Merges `other` headers into `self`.
@@ -169,45 +214,23 @@ module HTTP
169
214
  dup.tap { |dupped| dupped.merge! other }
170
215
  end
171
216
 
172
- class << self
173
- # Coerces given `object` into Headers.
174
- #
175
- # @raise [Error] if object can't be coerced
176
- # @param [#to_hash, #to_h, #to_a] object
177
- # @return [Headers]
178
- def coerce(object)
179
- unless object.is_a? self
180
- object = case
181
- when object.respond_to?(:to_hash) then object.to_hash
182
- when object.respond_to?(:to_h) then object.to_h
183
- when object.respond_to?(:to_a) then object.to_a
184
- else raise Error, "Can't coerce #{object.inspect} to Headers"
185
- end
186
- end
187
-
188
- headers = new
189
- object.each { |k, v| headers.add k, v }
190
- headers
191
- end
192
- alias [] coerce
193
- end
194
-
195
217
  private
196
218
 
197
219
  # Transforms `name` to canonical HTTP header capitalization
198
- #
199
- # @param [String] name
200
- # @raise [HeaderError] if normalized name does not
201
- # match {HEADER_NAME_RE}
202
- # @return [String] canonical HTTP header name
203
220
  def normalize_header(name)
204
- return name if name =~ CANONICAL_NAME_RE
205
-
206
- normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
221
+ self.class.normalizer.call(name)
222
+ end
207
223
 
208
- return normalized if normalized =~ COMPLIANT_NAME_RE
224
+ # Ensures there is no new line character in the header value
225
+ #
226
+ # @param [String] value
227
+ # @raise [HeaderError] if value includes new line character
228
+ # @return [String] stringified header value
229
+ def validate_value(value)
230
+ v = value.to_s
231
+ return v unless v.include?("\n")
209
232
 
210
- raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
233
+ raise HeaderError, "Invalid HTTP header field value: #{v.inspect}"
211
234
  end
212
235
  end
213
236
  end
@@ -14,13 +14,15 @@ module HTTP
14
14
  def_delegators :instance, :encode, :decode
15
15
  end
16
16
 
17
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
17
18
  %w[encode decode].each do |operation|
18
- class_eval <<-RUBY, __FILE__, __LINE__
19
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
20
  def #{operation}(*)
20
21
  fail Error, "\#{self.class} does not supports ##{operation}"
21
22
  end
22
23
  RUBY
23
24
  end
25
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
24
26
  end
25
27
  end
26
28
  end
@@ -10,6 +10,7 @@ module HTTP
10
10
  # Encodes object to JSON
11
11
  def encode(obj)
12
12
  return obj.to_json if obj.respond_to?(:to_json)
13
+
13
14
  ::JSON.dump obj
14
15
  end
15
16