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.
@@ -1,24 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ClassLength, Style/RedundantSelf
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
- attr_accessor name
43
- protected :"#{name}="
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 |headers|
74
- self.headers.merge(headers)
75
+ def_option :headers do |new_headers|
76
+ headers.merge(new_headers)
75
77
  end
76
78
 
77
- def_option :cookies do |cookies|
78
- cookies.each_with_object self.cookies.dup do |(k, v), jar|
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 |features|
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
- features = features.each_with_object({}) do |feature, h|
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
- self.features.merge(features)
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 follow response
123
+ proxy params form json body response
122
124
  socket_class nodelay ssl_socket_class ssl_context ssl
123
- persistent keep_alive_timeout timeout_class timeout_options
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
@@ -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 = request_body(opts[:body], opts)
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 => verb,
104
- :uri => @uri.join(uri),
105
- :headers => headers,
106
- :proxy => proxy,
107
- :body => body,
108
- :version => 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
@@ -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!
@@ -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
- # It's important to send the request in a single write call when
65
- # possible in order to play nicely with Nagle's algorithm. Making
66
- # two writes in a row triggers a pathological case where Nagle is
67
- # expecting a third write that never happens.
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
- write(data)
82
+ yield data
73
83
  data.clear
74
84
  end
75
85
 
76
- write(data) unless data.empty?
86
+ yield data unless data.empty?
77
87
 
78
- write(CHUNKED_END) if chunked?
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
@@ -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?(:connection)
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(stream, :encoding => encoding)
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
@@ -3,27 +3,25 @@
3
3
  require "timeout"
4
4
  require "io/wait"
5
5
 
6
- require "http/timeout/per_operation"
6
+ require "http/timeout/null"
7
7
 
8
8
  module HTTP
9
9
  module Timeout
10
- class Global < PerOperation
11
- attr_reader :time_left, :total_timeout
12
-
10
+ class Global < Null
13
11
  def initialize(*args)
14
12
  super
15
- reset_counter
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 = connect_timeout + read_timeout + write_timeout
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 #{total_timeout} seconds"
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "3.3.0"
4
+ VERSION = "4.0.0"
5
5
  end
@@ -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 "stream_for" do
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 connection" do
19
- stream = subject.stream_for(connection, response)
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 => "not-supported"} }
26
+ let(:headers) { {:content_encoding => "identity"} }
26
27
 
27
- it "returns connection" do
28
- stream = subject.stream_for(connection, response)
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 connection" do
37
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
46
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
55
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
64
- stream = subject.stream_for(connection, response)
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