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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- 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 = {})
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
data/lib/http/headers/known.rb
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 [
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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 { |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
|
|
221
|
+
self.class.normalizer.call(name)
|
|
222
|
+
end
|
|
207
223
|
|
|
208
|
-
|
|
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
|
|
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
|