http 4.4.0 → 5.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop.yml +8 -110
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop_todo.yml +192 -0
- data/.yardopts +1 -1
- data/CHANGES.md +90 -0
- data/Gemfile +18 -10
- data/README.md +17 -20
- data/Rakefile +2 -10
- data/http.gemspec +3 -3
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +36 -30
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -5
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +5 -8
- data/lib/http/redirector.rb +2 -1
- data/lib/http/request.rb +13 -10
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +3 -2
- data/lib/http/response.rb +17 -15
- data/lib/http/response/body.rb +2 -2
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +75 -49
- data/lib/http/response/status.rb +4 -3
- data/lib/http/timeout/global.rb +17 -31
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +31 -54
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +119 -30
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
- data/spec/lib/http/features/instrumentation_spec.rb +28 -21
- data/spec/lib/http/features/logging_spec.rb +8 -9
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +2 -1
- data/spec/lib/http/request/writer_spec.rb +13 -1
- data/spec/lib/http/request_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 +11 -22
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +4 -4
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +21 -14
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
data/lib/http/connection.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
require "http/headers"
|
6
|
-
require "http/response/parser"
|
7
6
|
|
8
7
|
module HTTP
|
9
8
|
# A connection to the HTTP server
|
@@ -45,8 +44,8 @@ module HTTP
|
|
45
44
|
send_proxy_connect_request(req)
|
46
45
|
start_tls(req, options)
|
47
46
|
reset_timer
|
48
|
-
rescue IOError, SocketError, SystemCallError =>
|
49
|
-
raise ConnectionError, "failed to connect: #{
|
47
|
+
rescue IOError, SocketError, SystemCallError => e
|
48
|
+
raise ConnectionError, "failed to connect: #{e}", e.backtrace
|
50
49
|
end
|
51
50
|
|
52
51
|
# @see (HTTP::Response::Parser#status_code)
|
@@ -68,8 +67,13 @@ module HTTP
|
|
68
67
|
# @param [Request] req Request to send to the server
|
69
68
|
# @return [nil]
|
70
69
|
def send_request(req)
|
71
|
-
|
72
|
-
|
70
|
+
if @pending_response
|
71
|
+
raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
|
72
|
+
end
|
73
|
+
|
74
|
+
if @pending_request
|
75
|
+
raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
|
76
|
+
end
|
73
77
|
|
74
78
|
@pending_request = true
|
75
79
|
|
@@ -216,8 +220,8 @@ module HTTP
|
|
216
220
|
elsif value
|
217
221
|
@parser << value
|
218
222
|
end
|
219
|
-
rescue IOError, SocketError, SystemCallError =>
|
220
|
-
raise ConnectionError, "error reading from socket: #{
|
223
|
+
rescue IOError, SocketError, SystemCallError => e
|
224
|
+
raise ConnectionError, "error reading from socket: #{e}", e.backtrace
|
221
225
|
end
|
222
226
|
end
|
223
227
|
end
|
data/lib/http/content_type.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTP
|
4
|
-
ContentType
|
5
|
-
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
6
|
-
CHARSET_RE = /;\s*charset=([^;]+)/i
|
4
|
+
class ContentType
|
5
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
|
6
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
|
7
|
+
|
8
|
+
attr_accessor :mime_type, :charset
|
7
9
|
|
8
10
|
class << self
|
9
11
|
# Parse string and return ContentType struct
|
@@ -15,15 +17,18 @@ module HTTP
|
|
15
17
|
|
16
18
|
# :nodoc:
|
17
19
|
def mime_type(str)
|
18
|
-
|
19
|
-
m && m.strip.downcase
|
20
|
+
str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
|
20
21
|
end
|
21
22
|
|
22
23
|
# :nodoc:
|
23
24
|
def charset(str)
|
24
|
-
|
25
|
-
m && m.strip.delete('"')
|
25
|
+
str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
|
26
26
|
end
|
27
27
|
end
|
28
|
+
|
29
|
+
def initialize(mime_type = nil, charset = nil)
|
30
|
+
@mime_type = mime_type
|
31
|
+
@charset = charset
|
32
|
+
end
|
28
33
|
end
|
29
34
|
end
|
data/lib/http/feature.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
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
8
|
|
@@ -13,6 +13,8 @@ module HTTP
|
|
13
13
|
def wrap_response(response)
|
14
14
|
response
|
15
15
|
end
|
16
|
+
|
17
|
+
def on_error(request, error); end
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
@@ -27,12 +27,12 @@ module HTTP
|
|
27
27
|
request.headers[Headers::CONTENT_ENCODING] = method
|
28
28
|
|
29
29
|
Request.new(
|
30
|
-
:version
|
31
|
-
:verb
|
32
|
-
:uri
|
33
|
-
:headers
|
34
|
-
:proxy
|
35
|
-
:body
|
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
36
|
:uri_normalizer => request.uri_normalizer
|
37
37
|
)
|
38
38
|
end
|
@@ -12,12 +12,13 @@ module HTTP
|
|
12
12
|
return response unless supported_encoding?(response)
|
13
13
|
|
14
14
|
options = {
|
15
|
-
:status
|
16
|
-
:version
|
17
|
-
:headers
|
15
|
+
:status => response.status,
|
16
|
+
:version => response.version,
|
17
|
+
:headers => response.headers,
|
18
18
|
:proxy_headers => response.proxy_headers,
|
19
|
-
:connection
|
20
|
-
:body
|
19
|
+
:connection => response.connection,
|
20
|
+
:body => stream_for(response.connection),
|
21
|
+
:request => response.request
|
21
22
|
}
|
22
23
|
|
23
24
|
options[:uri] = response.uri if response.uri
|
@@ -29,7 +29,7 @@ module HTTP
|
|
29
29
|
def wrap_request(request)
|
30
30
|
# Emit a separate "start" event, so a logger can print the request
|
31
31
|
# being run without waiting for a response
|
32
|
-
instrumenter.instrument("start_#{name}", :request => request)
|
32
|
+
instrumenter.instrument("start_#{name}", :request => request)
|
33
33
|
instrumenter.start(name, :request => request)
|
34
34
|
request
|
35
35
|
end
|
@@ -9,6 +9,20 @@ module HTTP
|
|
9
9
|
# HTTP.use(logging: {logger: Logger.new(STDOUT)}).get("https://example.com/")
|
10
10
|
#
|
11
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
|
+
|
12
26
|
attr_reader :logger
|
13
27
|
|
14
28
|
def initialize(logger: NullLogger.new)
|
@@ -17,39 +31,23 @@ module HTTP
|
|
17
31
|
|
18
32
|
def wrap_request(request)
|
19
33
|
logger.info { "> #{request.verb.to_s.upcase} #{request.uri}" }
|
20
|
-
logger.debug
|
21
|
-
headers = request.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
22
|
-
body = request.body.source
|
34
|
+
logger.debug { "#{stringify_headers(request.headers)}\n\n#{request.body.source}" }
|
23
35
|
|
24
|
-
headers + "\n\n" + body.to_s
|
25
|
-
end
|
26
36
|
request
|
27
37
|
end
|
28
38
|
|
29
39
|
def wrap_response(response)
|
30
40
|
logger.info { "< #{response.status}" }
|
31
|
-
logger.debug
|
32
|
-
headers = response.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
33
|
-
body = response.body.to_s
|
41
|
+
logger.debug { "#{stringify_headers(response.headers)}\n\n#{response.body}" }
|
34
42
|
|
35
|
-
headers + "\n\n" + body
|
36
|
-
end
|
37
43
|
response
|
38
44
|
end
|
39
45
|
|
40
|
-
|
41
|
-
%w[fatal error warn info debug].each do |level|
|
42
|
-
define_method(level.to_sym) do |*_args|
|
43
|
-
nil
|
44
|
-
end
|
46
|
+
private
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|
48
|
+
def stringify_headers(headers)
|
49
|
+
headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
50
50
|
end
|
51
|
-
|
52
|
-
HTTP::Options.register_feature(:logging, self)
|
53
51
|
end
|
54
52
|
end
|
55
53
|
end
|
data/lib/http/headers.rb
CHANGED
@@ -13,14 +13,18 @@ module HTTP
|
|
13
13
|
include Enumerable
|
14
14
|
|
15
15
|
# Matches HTTP header names when in "Canonical-Http-Format"
|
16
|
-
CANONICAL_NAME_RE =
|
16
|
+
CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/.freeze
|
17
17
|
|
18
18
|
# Matches valid header field name according to RFC.
|
19
19
|
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
20
|
-
COMPLIANT_NAME_RE =
|
20
|
+
COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/.freeze
|
21
21
|
|
22
22
|
# Class constructor.
|
23
23
|
def initialize
|
24
|
+
# The @pile stores each header value using a three element array:
|
25
|
+
# 0 - the normalized header key, used for lookup
|
26
|
+
# 1 - the header key as it will be sent with a request
|
27
|
+
# 2 - the value
|
24
28
|
@pile = []
|
25
29
|
end
|
26
30
|
|
@@ -45,12 +49,31 @@ module HTTP
|
|
45
49
|
|
46
50
|
# Appends header.
|
47
51
|
#
|
48
|
-
# @param [
|
52
|
+
# @param [String, Symbol] name header name. When specified as a string, the
|
53
|
+
# name is sent as-is. When specified as a symbol, the name is converted
|
54
|
+
# to a string of capitalized words separated by a dash. Word boundaries
|
55
|
+
# are determined by an underscore (`_`) or a dash (`-`).
|
56
|
+
# Ex: `:content_type` is sent as `"Content-Type"`, and `"auth_key"` (string)
|
57
|
+
# is sent as `"auth_key"`.
|
49
58
|
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
|
50
59
|
# @return [void]
|
51
60
|
def add(name, value)
|
52
|
-
|
53
|
-
|
61
|
+
lookup_name = normalize_header(name.to_s)
|
62
|
+
wire_name = case name
|
63
|
+
when String
|
64
|
+
name
|
65
|
+
when Symbol
|
66
|
+
lookup_name
|
67
|
+
else
|
68
|
+
raise HTTP::HeaderError, "HTTP header must be a String or Symbol: #{name.inspect}"
|
69
|
+
end
|
70
|
+
Array(value).each do |v|
|
71
|
+
@pile << [
|
72
|
+
lookup_name,
|
73
|
+
wire_name,
|
74
|
+
validate_value(v)
|
75
|
+
]
|
76
|
+
end
|
54
77
|
end
|
55
78
|
|
56
79
|
# Returns list of header values if any.
|
@@ -58,7 +81,7 @@ module HTTP
|
|
58
81
|
# @return [Array<String>]
|
59
82
|
def get(name)
|
60
83
|
name = normalize_header name.to_s
|
61
|
-
@pile.select { |k, _| k == name }.map { |_, v| v }
|
84
|
+
@pile.select { |k, _| k == name }.map { |_, _, v| v }
|
62
85
|
end
|
63
86
|
|
64
87
|
# Smart version of {#get}.
|
@@ -88,7 +111,7 @@ module HTTP
|
|
88
111
|
#
|
89
112
|
# @return [Hash]
|
90
113
|
def to_h
|
91
|
-
|
114
|
+
keys.map { |k| [k, self[k]] }.to_h
|
92
115
|
end
|
93
116
|
alias to_hash to_h
|
94
117
|
|
@@ -96,7 +119,7 @@ module HTTP
|
|
96
119
|
#
|
97
120
|
# @return [Array<[String, String]>]
|
98
121
|
def to_a
|
99
|
-
@pile.map { |
|
122
|
+
@pile.map { |item| item[1..2] }
|
100
123
|
end
|
101
124
|
|
102
125
|
# Returns human-readable representation of `self` instance.
|
@@ -110,7 +133,7 @@ module HTTP
|
|
110
133
|
#
|
111
134
|
# @return [Array<String>]
|
112
135
|
def keys
|
113
|
-
@pile.map { |k, _| k }.uniq
|
136
|
+
@pile.map { |_, k, _| k }.uniq
|
114
137
|
end
|
115
138
|
|
116
139
|
# Compares headers to another Headers or Array of key/value pairs
|
@@ -118,7 +141,8 @@ module HTTP
|
|
118
141
|
# @return [Boolean]
|
119
142
|
def ==(other)
|
120
143
|
return false unless other.respond_to? :to_a
|
121
|
-
|
144
|
+
|
145
|
+
to_a == other.to_a
|
122
146
|
end
|
123
147
|
|
124
148
|
# Calls the given block once for each key/value pair in headers container.
|
@@ -127,7 +151,8 @@ module HTTP
|
|
127
151
|
# @return [Headers] self-reference
|
128
152
|
def each
|
129
153
|
return to_enum(__method__) unless block_given?
|
130
|
-
|
154
|
+
|
155
|
+
@pile.each { |item| yield(item[1..2]) }
|
131
156
|
self
|
132
157
|
end
|
133
158
|
|
@@ -139,7 +164,7 @@ module HTTP
|
|
139
164
|
|
140
165
|
# @!method hash
|
141
166
|
# Compute a hash-code for this headers container.
|
142
|
-
# Two
|
167
|
+
# Two containers with the same content will have the same hash code.
|
143
168
|
#
|
144
169
|
# @see http://www.ruby-doc.org/core/Object.html#method-i-hash
|
145
170
|
# @return [Fixnum]
|
@@ -150,7 +175,7 @@ module HTTP
|
|
150
175
|
# @api private
|
151
176
|
def initialize_copy(orig)
|
152
177
|
super
|
153
|
-
@pile =
|
178
|
+
@pile = @pile.map(&:dup)
|
154
179
|
end
|
155
180
|
|
156
181
|
# Merges `other` headers into `self`.
|
@@ -209,5 +234,17 @@ module HTTP
|
|
209
234
|
|
210
235
|
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
|
211
236
|
end
|
237
|
+
|
238
|
+
# Ensures there is no new line character in the header value
|
239
|
+
#
|
240
|
+
# @param [String] value
|
241
|
+
# @raise [HeaderError] if value includes new line character
|
242
|
+
# @return [String] stringified header value
|
243
|
+
def validate_value(value)
|
244
|
+
v = value.to_s
|
245
|
+
return v unless v.include?("\n")
|
246
|
+
|
247
|
+
raise HeaderError, "Invalid HTTP header field value: #{v.inspect}"
|
248
|
+
end
|
212
249
|
end
|
213
250
|
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
|
data/lib/http/mime_type/json.rb
CHANGED
data/lib/http/options.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/ClassLength
|
4
|
-
|
5
3
|
require "http/headers"
|
6
4
|
require "openssl"
|
7
5
|
require "socket"
|
8
6
|
require "http/uri"
|
9
7
|
|
10
8
|
module HTTP
|
11
|
-
class Options
|
9
|
+
class Options # rubocop:disable Metrics/ClassLength
|
12
10
|
@default_socket_class = TCPSocket
|
13
11
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
14
12
|
@default_timeout_class = HTTP::Timeout::Null
|
@@ -18,9 +16,8 @@ module HTTP
|
|
18
16
|
attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
|
19
17
|
attr_reader :available_features
|
20
18
|
|
21
|
-
def new(options = {})
|
22
|
-
|
23
|
-
super
|
19
|
+
def new(options = {})
|
20
|
+
options.is_a?(self) ? options : super
|
24
21
|
end
|
25
22
|
|
26
23
|
def defined_options
|
@@ -35,7 +32,7 @@ module HTTP
|
|
35
32
|
|
36
33
|
def def_option(name, reader_only: false, &interpreter)
|
37
34
|
defined_options << name.to_sym
|
38
|
-
interpreter ||=
|
35
|
+
interpreter ||= ->(v) { v }
|
39
36
|
|
40
37
|
if reader_only
|
41
38
|
attr_reader name
|
@@ -50,7 +47,7 @@ module HTTP
|
|
50
47
|
end
|
51
48
|
end
|
52
49
|
|
53
|
-
def initialize(options = {})
|
50
|
+
def initialize(options = {})
|
54
51
|
defaults = {
|
55
52
|
:response => :auto,
|
56
53
|
:proxy => {},
|
data/lib/http/redirector.rb
CHANGED
@@ -39,7 +39,7 @@ module HTTP
|
|
39
39
|
# @param [Hash] opts
|
40
40
|
# @option opts [Boolean] :strict (true) redirector hops policy
|
41
41
|
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
|
42
|
-
def initialize(opts = {})
|
42
|
+
def initialize(opts = {})
|
43
43
|
@strict = opts.fetch(:strict, true)
|
44
44
|
@max_hops = opts.fetch(:max_hops, 5).to_i
|
45
45
|
end
|
@@ -90,6 +90,7 @@ module HTTP
|
|
90
90
|
|
91
91
|
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
92
92
|
raise StateError, "can't follow #{@response.status} redirect" if @strict
|
93
|
+
|
93
94
|
verb = :get
|
94
95
|
end
|
95
96
|
|