http 3.3.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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