http 3.3.0 → 4.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/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +6 -6
- data/CHANGES.md +41 -0
- data/README.md +7 -4
- data/Rakefile +1 -1
- data/lib/http.rb +1 -0
- data/lib/http/chainable.rb +14 -19
- data/lib/http/client.rb +11 -4
- data/lib/http/connection.rb +4 -8
- data/lib/http/feature.rb +13 -0
- data/lib/http/features/auto_deflate.rb +20 -5
- data/lib/http/features/auto_inflate.rb +16 -6
- data/lib/http/features/instrumentation.rb +56 -0
- data/lib/http/features/logging.rb +55 -0
- data/lib/http/options.rb +26 -20
- data/lib/http/request.rb +7 -14
- data/lib/http/request/body.rb +5 -0
- data/lib/http/request/writer.rb +21 -7
- data/lib/http/response.rb +7 -15
- data/lib/http/timeout/global.rb +12 -14
- data/lib/http/timeout/per_operation.rb +5 -7
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/features/auto_inflate_spec.rb +17 -21
- data/spec/lib/http/features/instrumentation_spec.rb +56 -0
- data/spec/lib/http/features/logging_spec.rb +74 -0
- data/spec/lib/http/request/body_spec.rb +29 -0
- data/spec/lib/http/request/writer_spec.rb +20 -0
- data/spec/lib/http_spec.rb +25 -65
- data/spec/support/http_handling_shared.rb +60 -64
- metadata +8 -3
- data/.ruby-version +0 -1
data/lib/http/options.rb
CHANGED
@@ -1,24 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/ClassLength
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
4
|
|
5
5
|
require "http/headers"
|
6
6
|
require "openssl"
|
7
7
|
require "socket"
|
8
8
|
require "http/uri"
|
9
|
-
require "http/feature"
|
10
|
-
require "http/features/auto_inflate"
|
11
|
-
require "http/features/auto_deflate"
|
12
9
|
|
13
10
|
module HTTP
|
14
11
|
class Options
|
15
12
|
@default_socket_class = TCPSocket
|
16
13
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
17
14
|
@default_timeout_class = HTTP::Timeout::Null
|
18
|
-
@available_features = {
|
19
|
-
:auto_inflate => Features::AutoInflate,
|
20
|
-
:auto_deflate => Features::AutoDeflate
|
21
|
-
}
|
15
|
+
@available_features = {}
|
22
16
|
|
23
17
|
class << self
|
24
18
|
attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
|
@@ -33,14 +27,22 @@ module HTTP
|
|
33
27
|
@defined_options ||= []
|
34
28
|
end
|
35
29
|
|
30
|
+
def register_feature(name, impl)
|
31
|
+
@available_features[name] = impl
|
32
|
+
end
|
33
|
+
|
36
34
|
protected
|
37
35
|
|
38
|
-
def def_option(name, &interpreter)
|
36
|
+
def def_option(name, reader_only: false, &interpreter)
|
39
37
|
defined_options << name.to_sym
|
40
38
|
interpreter ||= lambda { |v| v }
|
41
39
|
|
42
|
-
|
43
|
-
|
40
|
+
if reader_only
|
41
|
+
attr_reader name
|
42
|
+
else
|
43
|
+
attr_accessor name
|
44
|
+
protected :"#{name}="
|
45
|
+
end
|
44
46
|
|
45
47
|
define_method(:"with_#{name}") do |value|
|
46
48
|
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
|
@@ -70,12 +72,12 @@ module HTTP
|
|
70
72
|
opts_w_defaults.each { |(k, v)| self[k] = v }
|
71
73
|
end
|
72
74
|
|
73
|
-
def_option :headers do |
|
74
|
-
|
75
|
+
def_option :headers do |new_headers|
|
76
|
+
headers.merge(new_headers)
|
75
77
|
end
|
76
78
|
|
77
|
-
def_option :cookies do |
|
78
|
-
|
79
|
+
def_option :cookies do |new_cookies|
|
80
|
+
new_cookies.each_with_object cookies.dup do |(k, v), jar|
|
79
81
|
cookie = k.is_a?(Cookie) ? k : Cookie.new(k.to_s, v.to_s)
|
80
82
|
jar[cookie.name] = cookie.cookie_value
|
81
83
|
end
|
@@ -85,7 +87,7 @@ module HTTP
|
|
85
87
|
self.encoding = Encoding.find(encoding)
|
86
88
|
end
|
87
89
|
|
88
|
-
def_option :features do |
|
90
|
+
def_option :features, :reader_only => true do |new_features|
|
89
91
|
# Normalize features from:
|
90
92
|
#
|
91
93
|
# [{feature_one: {opt: 'val'}}, :feature_two]
|
@@ -93,7 +95,7 @@ module HTTP
|
|
93
95
|
# into:
|
94
96
|
#
|
95
97
|
# {feature_one: {opt: 'val'}, feature_two: {}}
|
96
|
-
|
98
|
+
normalized_features = new_features.each_with_object({}) do |feature, h|
|
97
99
|
if feature.is_a?(Hash)
|
98
100
|
h.merge!(feature)
|
99
101
|
else
|
@@ -101,7 +103,7 @@ module HTTP
|
|
101
103
|
end
|
102
104
|
end
|
103
105
|
|
104
|
-
|
106
|
+
features.merge(normalized_features)
|
105
107
|
end
|
106
108
|
|
107
109
|
def features=(features)
|
@@ -118,13 +120,15 @@ module HTTP
|
|
118
120
|
end
|
119
121
|
|
120
122
|
%w[
|
121
|
-
proxy params form json body
|
123
|
+
proxy params form json body response
|
122
124
|
socket_class nodelay ssl_socket_class ssl_context ssl
|
123
|
-
|
125
|
+
keep_alive_timeout timeout_class timeout_options
|
124
126
|
].each do |method_name|
|
125
127
|
def_option method_name
|
126
128
|
end
|
127
129
|
|
130
|
+
def_option :follow, :reader_only => true
|
131
|
+
|
128
132
|
def follow=(value)
|
129
133
|
@follow =
|
130
134
|
case
|
@@ -135,6 +139,8 @@ module HTTP
|
|
135
139
|
end
|
136
140
|
end
|
137
141
|
|
142
|
+
def_option :persistent, :reader_only => true
|
143
|
+
|
138
144
|
def persistent=(value)
|
139
145
|
@persistent = value ? HTTP::URI.parse(value).origin : nil
|
140
146
|
end
|
data/lib/http/request.rb
CHANGED
@@ -86,7 +86,7 @@ module HTTP
|
|
86
86
|
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
87
87
|
|
88
88
|
@proxy = opts[:proxy] || {}
|
89
|
-
@body =
|
89
|
+
@body = (body = opts[:body]).is_a?(Request::Body) ? body : Request::Body.new(body)
|
90
90
|
@version = opts[:version] || "1.1"
|
91
91
|
@headers = HTTP::Headers.coerce(opts[:headers] || {})
|
92
92
|
|
@@ -100,12 +100,12 @@ module HTTP
|
|
100
100
|
headers.delete(Headers::HOST)
|
101
101
|
|
102
102
|
self.class.new(
|
103
|
-
:verb
|
104
|
-
:uri
|
105
|
-
:headers
|
106
|
-
:proxy
|
107
|
-
:body
|
108
|
-
:version
|
103
|
+
:verb => verb,
|
104
|
+
:uri => @uri.join(uri),
|
105
|
+
:headers => headers,
|
106
|
+
:proxy => proxy,
|
107
|
+
:body => body.source,
|
108
|
+
:version => version
|
109
109
|
)
|
110
110
|
end
|
111
111
|
|
@@ -186,13 +186,6 @@ module HTTP
|
|
186
186
|
|
187
187
|
private
|
188
188
|
|
189
|
-
# Transforms body to an object suitable for streaming.
|
190
|
-
def request_body(body, opts)
|
191
|
-
body = Request::Body.new(body) unless body.is_a?(Request::Body)
|
192
|
-
body = opts[:auto_deflate].deflated_body(body) if opts[:auto_deflate]
|
193
|
-
body
|
194
|
-
end
|
195
|
-
|
196
189
|
# @!attribute [r] host
|
197
190
|
# @return [String]
|
198
191
|
def_delegator :@uri, :host
|
data/lib/http/request/body.rb
CHANGED
@@ -41,6 +41,11 @@ module HTTP
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
# Request bodies are equivalent when they have the same source.
|
45
|
+
def ==(other)
|
46
|
+
self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
|
47
|
+
end
|
48
|
+
|
44
49
|
private
|
45
50
|
|
46
51
|
def validate_source_type!
|
data/lib/http/request/writer.rb
CHANGED
@@ -60,22 +60,32 @@ module HTTP
|
|
60
60
|
@request_header.join(CRLF) + CRLF * 2
|
61
61
|
end
|
62
62
|
|
63
|
+
# Writes HTTP request data into the socket.
|
63
64
|
def send_request
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
|
65
|
+
each_chunk { |chunk| write chunk }
|
66
|
+
rescue Errno::EPIPE
|
67
|
+
# server doesn't need any more data
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Yields chunks of request data that should be sent to the socket.
|
72
|
+
#
|
73
|
+
# It's important to send the request in a single write call when possible
|
74
|
+
# in order to play nicely with Nagle's algorithm. Making two writes in a
|
75
|
+
# row triggers a pathological case where Nagle is expecting a third write
|
76
|
+
# that never happens.
|
77
|
+
def each_chunk
|
68
78
|
data = join_headers
|
69
79
|
|
70
80
|
@body.each do |chunk|
|
71
81
|
data << encode_chunk(chunk)
|
72
|
-
|
82
|
+
yield data
|
73
83
|
data.clear
|
74
84
|
end
|
75
85
|
|
76
|
-
|
86
|
+
yield data unless data.empty?
|
77
87
|
|
78
|
-
|
88
|
+
yield CHUNKED_END if chunked?
|
79
89
|
end
|
80
90
|
|
81
91
|
# Returns the chunk encoded for to the specified "Transfer-Encoding" header.
|
@@ -100,6 +110,10 @@ module HTTP
|
|
100
110
|
break unless data.bytesize > length
|
101
111
|
data = data.byteslice(length..-1)
|
102
112
|
end
|
113
|
+
rescue Errno::EPIPE
|
114
|
+
raise
|
115
|
+
rescue IOError, SocketError, SystemCallError => ex
|
116
|
+
raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace
|
103
117
|
end
|
104
118
|
end
|
105
119
|
end
|
data/lib/http/response.rb
CHANGED
@@ -20,6 +20,9 @@ module HTTP
|
|
20
20
|
# @return [Status]
|
21
21
|
attr_reader :status
|
22
22
|
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :version
|
25
|
+
|
23
26
|
# @return [Body]
|
24
27
|
attr_reader :body
|
25
28
|
|
@@ -46,14 +49,13 @@ module HTTP
|
|
46
49
|
@headers = HTTP::Headers.coerce(opts[:headers] || {})
|
47
50
|
@proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
|
48
51
|
|
49
|
-
if opts.include?(:
|
52
|
+
if opts.include?(:body)
|
53
|
+
@body = opts.fetch(:body)
|
54
|
+
else
|
50
55
|
connection = opts.fetch(:connection)
|
51
56
|
encoding = opts[:encoding] || charset || Encoding::BINARY
|
52
|
-
stream = body_stream_for(connection, opts)
|
53
57
|
|
54
|
-
@body = Response::Body.new(
|
55
|
-
else
|
56
|
-
@body = opts.fetch(:body)
|
58
|
+
@body = Response::Body.new(connection, :encoding => encoding)
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -160,15 +162,5 @@ module HTTP
|
|
160
162
|
def inspect
|
161
163
|
"#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
|
162
164
|
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
def body_stream_for(connection, opts)
|
167
|
-
if opts[:auto_inflate]
|
168
|
-
opts[:auto_inflate].stream_for(connection, self)
|
169
|
-
else
|
170
|
-
connection
|
171
|
-
end
|
172
|
-
end
|
173
165
|
end
|
174
166
|
end
|
data/lib/http/timeout/global.rb
CHANGED
@@ -3,27 +3,25 @@
|
|
3
3
|
require "timeout"
|
4
4
|
require "io/wait"
|
5
5
|
|
6
|
-
require "http/timeout/
|
6
|
+
require "http/timeout/null"
|
7
7
|
|
8
8
|
module HTTP
|
9
9
|
module Timeout
|
10
|
-
class Global <
|
11
|
-
attr_reader :time_left, :total_timeout
|
12
|
-
|
10
|
+
class Global < Null
|
13
11
|
def initialize(*args)
|
14
12
|
super
|
15
|
-
|
13
|
+
|
14
|
+
@timeout = @time_left = options.fetch(:global_timeout)
|
16
15
|
end
|
17
16
|
|
18
17
|
# To future me: Don't remove this again, past you was smarter.
|
19
18
|
def reset_counter
|
20
|
-
@time_left =
|
21
|
-
@total_timeout = time_left
|
19
|
+
@time_left = @timeout
|
22
20
|
end
|
23
21
|
|
24
22
|
def connect(socket_class, host, port, nodelay = false)
|
25
23
|
reset_timer
|
26
|
-
::Timeout.timeout(time_left, TimeoutError) do
|
24
|
+
::Timeout.timeout(@time_left, TimeoutError) do
|
27
25
|
@socket = socket_class.open(host, port)
|
28
26
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
29
27
|
end
|
@@ -37,11 +35,11 @@ module HTTP
|
|
37
35
|
begin
|
38
36
|
@socket.connect_nonblock
|
39
37
|
rescue IO::WaitReadable
|
40
|
-
IO.select([@socket], nil, nil, time_left)
|
38
|
+
IO.select([@socket], nil, nil, @time_left)
|
41
39
|
log_time
|
42
40
|
retry
|
43
41
|
rescue IO::WaitWritable
|
44
|
-
IO.select(nil, [@socket], nil, time_left)
|
42
|
+
IO.select(nil, [@socket], nil, @time_left)
|
45
43
|
log_time
|
46
44
|
retry
|
47
45
|
end
|
@@ -105,13 +103,13 @@ module HTTP
|
|
105
103
|
|
106
104
|
# Wait for a socket to become readable
|
107
105
|
def wait_readable_or_timeout
|
108
|
-
@socket.to_io.wait_readable(time_left)
|
106
|
+
@socket.to_io.wait_readable(@time_left)
|
109
107
|
log_time
|
110
108
|
end
|
111
109
|
|
112
110
|
# Wait for a socket to become writable
|
113
111
|
def wait_writable_or_timeout
|
114
|
-
@socket.to_io.wait_writable(time_left)
|
112
|
+
@socket.to_io.wait_writable(@time_left)
|
115
113
|
log_time
|
116
114
|
end
|
117
115
|
|
@@ -123,8 +121,8 @@ module HTTP
|
|
123
121
|
|
124
122
|
def log_time
|
125
123
|
@time_left -= (Time.now - @started)
|
126
|
-
if time_left <= 0
|
127
|
-
raise TimeoutError, "Timed out after using the allocated #{
|
124
|
+
if @time_left <= 0
|
125
|
+
raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
|
128
126
|
end
|
129
127
|
|
130
128
|
reset_timer
|
@@ -11,8 +11,6 @@ module HTTP
|
|
11
11
|
WRITE_TIMEOUT = 0.25
|
12
12
|
READ_TIMEOUT = 0.25
|
13
13
|
|
14
|
-
attr_reader :read_timeout, :write_timeout, :connect_timeout
|
15
|
-
|
16
14
|
def initialize(*args)
|
17
15
|
super
|
18
16
|
|
@@ -22,7 +20,7 @@ module HTTP
|
|
22
20
|
end
|
23
21
|
|
24
22
|
def connect(socket_class, host, port, nodelay = false)
|
25
|
-
::Timeout.timeout(connect_timeout, TimeoutError) do
|
23
|
+
::Timeout.timeout(@connect_timeout, TimeoutError) do
|
26
24
|
@socket = socket_class.open(host, port)
|
27
25
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
28
26
|
end
|
@@ -67,7 +65,7 @@ module HTTP
|
|
67
65
|
return :eof if result.nil?
|
68
66
|
return result if result != :wait_readable
|
69
67
|
|
70
|
-
raise TimeoutError, "Read timed out after #{read_timeout} seconds" if timeout
|
68
|
+
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
71
69
|
# marking the socket for timeout. Why is this not being raised immediately?
|
72
70
|
# it seems there is some race-condition on the network level between calling
|
73
71
|
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
@@ -78,7 +76,7 @@ module HTTP
|
|
78
76
|
# timeout. Else, the first timeout was a proper timeout.
|
79
77
|
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
80
78
|
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
81
|
-
timeout = true unless @socket.to_io.wait_readable(read_timeout)
|
79
|
+
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
82
80
|
end
|
83
81
|
end
|
84
82
|
|
@@ -89,9 +87,9 @@ module HTTP
|
|
89
87
|
result = @socket.write_nonblock(data, :exception => false)
|
90
88
|
return result unless result == :wait_writable
|
91
89
|
|
92
|
-
raise TimeoutError, "Write timed out after #{write_timeout} seconds" if timeout
|
90
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
93
91
|
|
94
|
-
timeout = true unless @socket.to_io.wait_writable(write_timeout)
|
92
|
+
timeout = true unless @socket.to_io.wait_writable(@write_timeout)
|
95
93
|
end
|
96
94
|
end
|
97
95
|
|
data/lib/http/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
RSpec.describe HTTP::Features::AutoInflate do
|
4
|
-
subject { HTTP::Features::AutoInflate.new }
|
4
|
+
subject(:feature) { HTTP::Features::AutoInflate.new }
|
5
5
|
let(:connection) { double }
|
6
6
|
let(:headers) { {} }
|
7
7
|
let(:response) do
|
@@ -13,56 +13,52 @@ RSpec.describe HTTP::Features::AutoInflate do
|
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
16
|
-
describe "
|
16
|
+
describe "wrap_response" do
|
17
|
+
subject(:result) { feature.wrap_response(response) }
|
18
|
+
|
17
19
|
context "when there is no Content-Encoding header" do
|
18
|
-
it "returns
|
19
|
-
|
20
|
-
expect(stream).to eq(connection)
|
20
|
+
it "returns original request" do
|
21
|
+
expect(result).to be response
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
25
|
context "for identity Content-Encoding header" do
|
25
|
-
let(:headers) { {:content_encoding => "
|
26
|
+
let(:headers) { {:content_encoding => "identity"} }
|
26
27
|
|
27
|
-
it "returns
|
28
|
-
|
29
|
-
expect(stream).to eq(connection)
|
28
|
+
it "returns original request" do
|
29
|
+
expect(result).to be response
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
context "for unknown Content-Encoding header" do
|
34
34
|
let(:headers) { {:content_encoding => "not-supported"} }
|
35
35
|
|
36
|
-
it "returns
|
37
|
-
|
38
|
-
expect(stream).to eq(connection)
|
36
|
+
it "returns original request" do
|
37
|
+
expect(result).to be response
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
42
41
|
context "for deflate Content-Encoding header" do
|
43
42
|
let(:headers) { {:content_encoding => "deflate"} }
|
44
43
|
|
45
|
-
it "returns HTTP::Response
|
46
|
-
|
47
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
44
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
45
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
48
46
|
end
|
49
47
|
end
|
50
48
|
|
51
49
|
context "for gzip Content-Encoding header" do
|
52
50
|
let(:headers) { {:content_encoding => "gzip"} }
|
53
51
|
|
54
|
-
it "returns HTTP::Response
|
55
|
-
|
56
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
52
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
53
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
57
54
|
end
|
58
55
|
end
|
59
56
|
|
60
57
|
context "for x-gzip Content-Encoding header" do
|
61
58
|
let(:headers) { {:content_encoding => "x-gzip"} }
|
62
59
|
|
63
|
-
it "returns HTTP::Response
|
64
|
-
|
65
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
60
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
61
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
66
62
|
end
|
67
63
|
end
|
68
64
|
end
|