excon 0.99.0 → 1.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.
data/excon.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  "README.md",
20
20
  "excon.gemspec"
21
21
  ]
22
+ s.required_ruby_version = '>= 2.7.0'
22
23
 
23
24
  s.add_development_dependency('rspec', '>= 3.5.0')
24
25
  s.add_development_dependency('activesupport')
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'ipaddr'
3
2
 
4
3
  module Excon
5
4
  class Connection
@@ -230,6 +229,13 @@ module Excon
230
229
  def request(params={}, &block)
231
230
  # @data has defaults, merge in new params to override
232
231
  datum = @data.merge(params)
232
+
233
+ # Set the deadline for the current request in order to determine when we have run out of time.
234
+ # Only set when a request timeout has been defined.
235
+ if datum[:timeout]
236
+ datum[:deadline] = Process.clock_gettime(Process::CLOCK_MONOTONIC) + datum[:timeout]
237
+ end
238
+
233
239
  datum[:headers] = @data[:headers].merge(datum[:headers] || {})
234
240
 
235
241
  validate_params(:request, params, datum[:middlewares])
@@ -244,16 +250,17 @@ module Excon
244
250
  datum[:headers]['Authorization'] ||= 'Basic ' + ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
245
251
  end
246
252
 
253
+ host_key = datum[:headers].keys.detect {|k| k.casecmp('Host') == 0 } || 'Host'
247
254
  if datum[:scheme] == UNIX
248
- datum[:headers]['Host'] ||= ''
255
+ datum[:headers][host_key] ||= ''
249
256
  else
250
- datum[:headers]['Host'] ||= datum[:host] + port_string(datum)
257
+ datum[:headers][host_key] ||= datum[:host] + port_string(datum)
251
258
  end
252
259
 
253
260
  # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present.
254
261
  # Some web servers will reject the request if it comes too late, so let's hoist it to the top.
255
- if (host = datum[:headers].delete('Host'))
256
- datum[:headers] = { 'Host' => host }.merge(datum[:headers])
262
+ if (host = datum[:headers].delete(host_key))
263
+ datum[:headers] = { host_key => host }.merge(datum[:headers])
257
264
  end
258
265
 
259
266
  # default to GET if no method specified
@@ -439,6 +446,12 @@ module Excon
439
446
  raise ArgumentError.new("Invalid validation type '#{validation}'")
440
447
  end
441
448
 
449
+ if validation == :connection && params[:omit_default_port] != true
450
+ Excon.display_warning(
451
+ 'The `omit_default_port` connection option is deprecated, please use `include_default_port` instead.'
452
+ )
453
+ end
454
+
442
455
  invalid_keys = params.keys - valid_keys
443
456
  unless invalid_keys.empty?
444
457
  Excon.display_warning("Invalid Excon #{validation} keys: #{invalid_keys.map(&:inspect).join(', ')}")
@@ -1,32 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
  module Excon
3
-
4
3
  CR_NL = "\r\n"
5
4
 
6
- DEFAULT_CA_FILE = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "data", "cacert.pem"))
5
+ DEFAULT_CA_FILE = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'cacert.pem'))
7
6
 
8
- DEFAULT_CHUNK_SIZE = 1048576 # 1 megabyte
7
+ DEFAULT_CHUNK_SIZE = 1_048_576 # 1 megabyte
9
8
 
10
9
  # avoid overwrite if somebody has redefined
11
- unless const_defined?(:CHUNK_SIZE)
12
- CHUNK_SIZE = DEFAULT_CHUNK_SIZE
13
- end
10
+ CHUNK_SIZE = DEFAULT_CHUNK_SIZE unless const_defined?(:CHUNK_SIZE)
14
11
 
15
12
  DEFAULT_REDIRECT_LIMIT = 10
16
13
 
17
14
  DEFAULT_RETRY_LIMIT = 4
18
15
 
19
16
  DEFAULT_RETRY_ERRORS = [
20
- Excon::Error::Timeout,
17
+ Excon::Error::RequestTimeout,
18
+ Excon::Error::Server,
21
19
  Excon::Error::Socket,
22
- Excon::Error::HTTPStatus
23
- ]
20
+ Excon::Error::Timeout,
21
+ Excon::Error::TooManyRequests
22
+ ].freeze
24
23
 
25
24
  FORCE_ENC = CR_NL.respond_to?(:force_encoding)
26
25
 
27
26
  HTTP_1_1 = " HTTP/1.1\r\n"
28
27
 
29
- HTTP_VERBS = %w{connect delete get head options patch post put trace}
28
+ HTTP_VERBS = %w[connect delete get head options patch post put trace].freeze
30
29
 
31
30
  HTTPS = 'https'
32
31
 
@@ -40,86 +39,90 @@ module Excon
40
39
 
41
40
  VERSIONS = "#{USER_AGENT} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}"
42
41
 
43
- VALID_REQUEST_KEYS = [
44
- :allow_unstubbed_requests,
45
- :body,
46
- :chunk_size,
47
- :debug_request,
48
- :debug_response,
49
- :headers,
50
- :instrumentor, # Used for setting logging within Connection
51
- :logger,
52
- :method,
53
- :middlewares,
54
- :password,
55
- :path,
56
- :persistent,
57
- :pipeline,
58
- :query,
59
- :read_timeout,
60
- :request_block,
61
- :response_block,
62
- :stubs,
63
- :user,
64
- :versions,
65
- :write_timeout
66
- ]
67
-
68
- VALID_CONNECTION_KEYS = VALID_REQUEST_KEYS + [
69
- :ciphers,
70
- :client_key,
71
- :client_key_data,
72
- :client_key_pass,
73
- :client_cert,
74
- :client_cert_data,
75
- :client_chain,
76
- :client_chain_data,
77
- :certificate,
78
- :certificate_path,
79
- :disable_proxy,
80
- :private_key,
81
- :private_key_path,
82
- :connect_timeout,
83
- :family,
84
- :keepalive,
85
- :host,
86
- :hostname,
87
- :omit_default_port,
88
- :nonblock,
89
- :reuseaddr,
90
- :port,
91
- :proxy,
92
- :scheme,
93
- :socket,
94
- :ssl_ca_file,
95
- :ssl_ca_path,
96
- :ssl_cert_store,
97
- :ssl_verify_callback,
98
- :ssl_verify_peer,
99
- :ssl_verify_peer_host,
100
- :ssl_verify_hostname,
101
- :ssl_version,
102
- :ssl_min_version,
103
- :ssl_max_version,
104
- :ssl_security_level,
105
- :ssl_proxy_headers,
106
- :ssl_uri_schemes,
107
- :tcp_nodelay,
108
- :thread_safe_sockets,
109
- :uri_parser,
42
+ VALID_REQUEST_KEYS = %i[
43
+ allow_unstubbed_requests
44
+ body
45
+ chunk_size
46
+ debug_request
47
+ debug_response
48
+ dns_timeouts
49
+ headers
50
+ instrumentor
51
+ logger
52
+ method
53
+ middlewares
54
+ password
55
+ path
56
+ persistent
57
+ pipeline
58
+ query
59
+ read_timeout
60
+ request_block
61
+ resolv_resolver
62
+ response_block
63
+ stubs
64
+ timeout
65
+ user
66
+ versions
67
+ write_timeout
68
+ ].freeze
69
+
70
+ VALID_CONNECTION_KEYS = VALID_REQUEST_KEYS + %i[
71
+ ciphers
72
+ client_key
73
+ client_key_data
74
+ client_key_pass
75
+ client_cert
76
+ client_cert_data
77
+ client_chain
78
+ client_chain_data
79
+ certificate
80
+ certificate_path
81
+ disable_proxy
82
+ private_key
83
+ private_key_path
84
+ connect_timeout
85
+ family
86
+ keepalive
87
+ host
88
+ hostname
89
+ include_default_port
90
+ omit_default_port
91
+ nonblock
92
+ reuseaddr
93
+ port
94
+ proxy
95
+ scheme
96
+ socket
97
+ ssl_ca_file
98
+ ssl_ca_path
99
+ ssl_cert_store
100
+ ssl_verify_callback
101
+ ssl_verify_peer
102
+ ssl_verify_peer_host
103
+ ssl_verify_hostname
104
+ ssl_version
105
+ ssl_min_version
106
+ ssl_max_version
107
+ ssl_security_level
108
+ ssl_proxy_headers
109
+ ssl_uri_schemes
110
+ tcp_nodelay
111
+ thread_safe_sockets
112
+ uri_parser
110
113
  ]
111
114
 
112
115
  DEPRECATED_VALID_REQUEST_KEYS = {
113
- :captures => 'Mock',
114
- :expects => 'Expects',
115
- :idempotent => 'Idempotent',
116
- :instrumentor_name => 'Instrumentor',
117
- :mock => 'Mock',
118
- :retries_remaining => 'Idempotent', # referenced in Instrumentor, but only relevant with Idempotent
119
- :retry_errors => 'Idempotent',
120
- :retry_interval => 'Idempotent',
121
- :retry_limit => 'Idempotent' # referenced in Instrumentor, but only relevant with Idempotent
122
- }
116
+ captures: 'Mock',
117
+ expects: 'Expects',
118
+ idempotent: 'Idempotent',
119
+ instrumentor_name: 'Instrumentor',
120
+ mock: 'Mock',
121
+ retries_remaining: 'Idempotent', # referenced in Instrumentor, but only relevant with Idempotent
122
+ retry_errors: 'Idempotent',
123
+ retry_interval: 'Idempotent',
124
+ retry_limit: 'Idempotent' # referenced in Instrumentor, but only relevant with Idempotent
125
+ }.freeze
123
126
 
124
127
  unless ::IO.const_defined?(:WaitReadable)
125
128
  class ::IO
@@ -132,43 +135,48 @@ module Excon
132
135
  module WaitWritable; end
133
136
  end
134
137
  end
138
+
135
139
  # these come last as they rely on the above
136
140
  DEFAULTS = {
137
- :chunk_size => CHUNK_SIZE || DEFAULT_CHUNK_SIZE,
141
+ chunk_size: CHUNK_SIZE || DEFAULT_CHUNK_SIZE,
138
142
  # see https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
139
143
  # list provided then had DES related things sorted to the end
140
- :ciphers => 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:DES-CBC3-SHA:!DSS',
141
- :connect_timeout => 60,
142
- :debug_request => false,
143
- :debug_response => false,
144
- :headers => {
144
+ ciphers: 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:DES-CBC3-SHA:!DSS',
145
+ connect_timeout: 60,
146
+ debug_request: false,
147
+ debug_response: false,
148
+ dns_timeouts: nil, # nil allows Resolv::DNS default timeout value (see https://ruby-doc.org/3.2.2/stdlibs/resolv/Resolv/DNS.html#method-i-timeouts-3D)
149
+ headers: {
145
150
  'User-Agent' => USER_AGENT,
146
- 'Accept' => '*/*'
151
+ 'Accept' => '*/*'
147
152
  },
148
- :idempotent => false,
149
- :instrumentor_name => 'excon',
150
- :middlewares => [
153
+ idempotent: false,
154
+ instrumentor_name: 'excon',
155
+ middlewares: [
151
156
  Excon::Middleware::ResponseParser,
157
+ Excon::Middleware::Decompress,
152
158
  Excon::Middleware::Expects,
153
159
  Excon::Middleware::Idempotent,
154
160
  Excon::Middleware::Instrumentor,
155
161
  Excon::Middleware::Mock
156
162
  ],
157
- :mock => false,
158
- :nonblock => true,
159
- :omit_default_port => false,
160
- :persistent => false,
161
- :read_timeout => 60,
162
- :retry_errors => DEFAULT_RETRY_ERRORS,
163
- :retry_limit => DEFAULT_RETRY_LIMIT,
164
- :ssl_verify_peer => true,
165
- :ssl_uri_schemes => [HTTPS],
166
- :stubs => :global,
167
- :tcp_nodelay => false,
168
- :thread_safe_sockets => true,
169
- :uri_parser => URI,
170
- :versions => VERSIONS,
171
- :write_timeout => 60
163
+ mock: false,
164
+ include_default_port: false,
165
+ nonblock: true,
166
+ omit_default_port: true,
167
+ persistent: false,
168
+ read_timeout: 60,
169
+ resolv_resolver: nil,
170
+ retry_errors: DEFAULT_RETRY_ERRORS,
171
+ retry_limit: DEFAULT_RETRY_LIMIT,
172
+ ssl_verify_peer: true,
173
+ ssl_uri_schemes: [HTTPS],
174
+ stubs: :global,
175
+ tcp_nodelay: false,
176
+ thread_safe_sockets: true,
177
+ timeout: nil,
178
+ uri_parser: URI,
179
+ versions: VERSIONS,
180
+ write_timeout: 60
172
181
  }
173
-
174
182
  end
@@ -1,41 +1,36 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Excon
3
4
  module Middleware
4
5
  class Decompress < Excon::Middleware::Base
5
-
6
6
  INFLATE_ZLIB_OR_GZIP = 47 # Zlib::MAX_WBITS + 32
7
7
  INFLATE_RAW = -15 # Zlib::MAX_WBITS * -1
8
8
 
9
9
  def request_call(datum)
10
- unless datum.has_key?(:response_block)
11
- key = datum[:headers].keys.detect {|k| k.to_s.casecmp('Accept-Encoding') == 0 } || 'Accept-Encoding'
12
- if datum[:headers][key].to_s.empty?
13
- datum[:headers][key] = 'deflate, gzip'
14
- end
10
+ unless datum.key?(:response_block)
11
+ key = datum[:headers].keys.detect { |k| k.to_s.casecmp('Accept-Encoding').zero? } || 'Accept-Encoding'
12
+ datum[:headers][key] = 'deflate, gzip' if datum[:headers][key].to_s.empty?
15
13
  end
16
14
  @stack.request_call(datum)
17
15
  end
18
16
 
19
17
  def response_call(datum)
20
18
  body = datum[:response][:body]
21
- unless datum.has_key?(:response_block) || body.nil? || body.empty?
22
- if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 })
23
- encodings = Utils.split_header_value(datum[:response][:headers][key])
24
- if (encoding = encodings.last)
25
- if encoding.casecmp('deflate') == 0
26
- datum[:response][:body] = begin
27
- Zlib::Inflate.new(INFLATE_ZLIB_OR_GZIP).inflate(body)
28
- rescue Zlib::DataError # fallback to raw on error
29
- Zlib::Inflate.new(INFLATE_RAW).inflate(body)
30
- end
31
- encodings.pop
32
- elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
33
- datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(body)).read
34
- encodings.pop
35
- end
36
- datum[:response][:headers][key] = encodings.join(', ')
19
+ if !(datum.key?(:response_block) || body.nil? || body.empty?) &&
20
+ (key = datum[:response][:headers].keys.detect { |k| k.casecmp('Content-Encoding').zero? })
21
+ encodings = Utils.split_header_value(datum[:response][:headers][key])
22
+ if encodings.last.casecmp('deflate').zero?
23
+ datum[:response][:body] = begin
24
+ Zlib::Inflate.new(INFLATE_ZLIB_OR_GZIP).inflate(body)
25
+ rescue Zlib::DataError # fallback to raw on error
26
+ Zlib::Inflate.new(INFLATE_RAW).inflate(body)
37
27
  end
28
+ encodings.pop
29
+ elsif encodings.last.casecmp('gzip').zero? || encodings.last.casecmp('x-gzip').zero?
30
+ datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(body)).read
31
+ encodings.pop
38
32
  end
33
+ datum[:response][:headers][key] = encodings.join(', ')
39
34
  end
40
35
  @stack.response_call(datum)
41
36
  end
data/lib/excon/socket.rb CHANGED
@@ -25,6 +25,13 @@ module Excon
25
25
  else # Ruby <= 2.0
26
26
  [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable]
27
27
  end
28
+ # Maps a socket operation to a timeout property.
29
+ OPERATION_TO_TIMEOUT = {
30
+ :connect_read => :connect_timeout,
31
+ :connect_write => :connect_timeout,
32
+ :read => :read_timeout,
33
+ :write => :write_timeout
34
+ }.freeze
28
35
 
29
36
  def params
30
37
  Excon.display_warning('Excon::Socket#params is deprecated use Excon::Socket#data instead.')
@@ -46,14 +53,16 @@ module Excon
46
53
  @nonblock = data[:nonblock]
47
54
  @port ||= @data[:port] || 80
48
55
  @read_buffer = String.new
56
+ @read_offset = 0
49
57
  @eof = false
50
58
  @backend_eof = false
59
+
51
60
  connect
52
61
  end
53
62
 
54
63
  def read(max_length = nil)
55
64
  if @eof
56
- return max_length ? nil : ''
65
+ max_length ? nil : ''
57
66
  elsif @nonblock
58
67
  read_nonblock(max_length)
59
68
  else
@@ -62,22 +71,24 @@ module Excon
62
71
  end
63
72
 
64
73
  def readline
65
- if @nonblock && RUBY_VERSION.to_f > 1.8_7
74
+ if @nonblock
66
75
  result = String.new
67
- block = @read_buffer
68
- @read_buffer = String.new
76
+ block = consume_read_buffer
69
77
 
70
78
  loop do
71
79
  idx = block.index("\n")
80
+
72
81
  if idx.nil?
73
82
  result << block
74
83
  else
75
- result << block.slice!(0, idx+1)
76
- add_to_read_buffer(block)
84
+ result << block[0..idx]
85
+ rewind_read_buffer(block, idx)
77
86
  break
78
87
  end
88
+
79
89
  block = read_nonblock(@data[:chunk_size]) || raise(EOFError)
80
90
  end
91
+
81
92
  result
82
93
  else # nonblock/legacy
83
94
  begin
@@ -121,7 +132,17 @@ module Excon
121
132
  family = @data[:proxy][:family]
122
133
  end
123
134
 
124
- Resolv.each_address(hostname) do |ip|
135
+ resolver = @data[:resolv_resolver] || Resolv::DefaultResolver
136
+
137
+ # Deprecated
138
+ if @data[:dns_timeouts]
139
+ Excon.display_warning('dns_timeouts is deprecated, use resolv_resolver instead.')
140
+ dns_resolver = Resolv::DNS.new
141
+ dns_resolver.timeouts = @data[:dns_timeouts]
142
+ resolver = Resolv.new([Resolv::Hosts.new, dns_resolver])
143
+ end
144
+
145
+ resolver.each_address(hostname) do |ip|
125
146
  # already succeeded on previous addrinfo
126
147
  if @socket
127
148
  break
@@ -187,20 +208,48 @@ module Excon
187
208
  end
188
209
  end
189
210
 
190
- def add_to_read_buffer(str)
191
- @read_buffer << str
211
+ # Consume any bytes remaining in the read buffer before making a system call.
212
+ def consume_read_buffer
213
+ block = @read_buffer[@read_offset..-1]
214
+
215
+ @read_offset = @read_buffer.length
216
+
217
+ block
218
+ end
219
+
220
+ # Rewind the read buffer to just after the given index.
221
+ # The offset is moved back to the start of the current chunk and then forward until just after the index.
222
+ def rewind_read_buffer(chunk, idx)
223
+ @read_offset = @read_offset - chunk.length + (idx + 1)
192
224
  @eof = false
193
225
  end
194
226
 
195
227
  def read_nonblock(max_length)
196
228
  begin
229
+ if @read_offset != 0 && @read_offset >= @read_buffer.length
230
+ # Clear the buffer so we can test for emptiness below
231
+ @read_buffer.clear
232
+ # Reset the offset so it matches the length of the buffer when empty.
233
+ @read_offset = 0
234
+ end
235
+
197
236
  if max_length
198
- until @backend_eof || @read_buffer.length >= max_length
199
- @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
237
+ until @backend_eof || readable_bytes >= max_length
238
+ if @read_buffer.empty?
239
+ # Avoid allocating a new buffer string when the read buffer is empty
240
+ @read_buffer = @socket.read_nonblock(max_length, @read_buffer)
241
+ else
242
+ @read_buffer << @socket.read_nonblock(max_length - readable_bytes)
243
+ end
200
244
  end
201
245
  else
202
- while !@backend_eof
203
- @read_buffer << @socket.read_nonblock(@data[:chunk_size])
246
+ until @backend_eof
247
+ if @read_buffer.empty?
248
+ # Avoid allocating a new buffer string when the read buffer is empty
249
+ @read_buffer = @socket.read_nonblock(@data[:chunk_size], @read_buffer)
250
+ else
251
+ @read_buffer << @socket.read_nonblock(@data[:chunk_size])
252
+ end
204
253
  end
205
254
  end
206
255
  rescue OpenSSL::SSL::SSLError => error
@@ -220,18 +269,32 @@ module Excon
220
269
  @backend_eof = true
221
270
  end
222
271
 
223
- ret = if max_length
272
+ if max_length
224
273
  if @read_buffer.empty?
225
- nil # EOF met at beginning
274
+ # EOF met at beginning
275
+ @eof = @backend_eof
276
+ nil
226
277
  else
227
- @read_buffer.slice!(0, max_length)
278
+ start = @read_offset
279
+
280
+ # Ensure that we can seek backwards when reading until a terminator string.
281
+ # The read offset must never point past the end of the read buffer.
282
+ @read_offset += max_length > readable_bytes ? readable_bytes : max_length
283
+ @read_buffer[start...@read_offset]
228
284
  end
229
285
  else
230
286
  # read until EOFError, so return everything
231
- @read_buffer.slice!(0, @read_buffer.length)
287
+ start = @read_offset
288
+
289
+ @read_offset = @read_buffer.length
290
+ @eof = @backend_eof
291
+
292
+ @read_buffer[start..-1]
232
293
  end
233
- @eof = @backend_eof && @read_buffer.empty?
234
- ret
294
+ end
295
+
296
+ def readable_bytes
297
+ @read_buffer.length - @read_offset
235
298
  end
236
299
 
237
300
  def read_block(max_length)
@@ -294,17 +357,33 @@ module Excon
294
357
  end
295
358
 
296
359
  def select_with_timeout(socket, type)
360
+ timeout_kind = type
361
+ timeout = @data[OPERATION_TO_TIMEOUT[type]]
362
+
363
+ # Check whether the request has a timeout configured.
364
+ if @data.include?(:deadline)
365
+ request_timeout = request_time_remaining
366
+
367
+ # If the time remaining until the request times out is less than the timeout for the type of select,
368
+ # use the time remaining as the timeout instead.
369
+ if request_timeout < timeout
370
+ timeout_kind = :request
371
+ timeout = request_timeout
372
+ end
373
+ end
374
+
297
375
  select = case type
298
376
  when :connect_read
299
- IO.select([socket], nil, nil, @data[:connect_timeout])
377
+ IO.select([socket], nil, nil, timeout)
300
378
  when :connect_write
301
- IO.select(nil, [socket], nil, @data[:connect_timeout])
379
+ IO.select(nil, [socket], nil, timeout)
302
380
  when :read
303
- IO.select([socket], nil, nil, @data[:read_timeout])
381
+ IO.select([socket], nil, nil, timeout)
304
382
  when :write
305
- IO.select(nil, [socket], nil, @data[:write_timeout])
383
+ IO.select(nil, [socket], nil, timeout)
306
384
  end
307
- select || raise(Excon::Errors::Timeout.new("#{type} timeout reached"))
385
+
386
+ select || raise(Excon::Errors::Timeout.new("#{timeout_kind} timeout reached"))
308
387
  end
309
388
 
310
389
  def unpacked_sockaddr
@@ -314,5 +393,16 @@ module Excon
314
393
  raise
315
394
  end
316
395
  end
396
+
397
+ # Returns the remaining time in seconds until we reach the deadline for the request timeout.
398
+ # Raises an exception if we have exceeded the request timeout's deadline.
399
+ def request_time_remaining
400
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
401
+ deadline = @data[:deadline]
402
+
403
+ raise(Excon::Errors::Timeout.new('request timeout reached')) if now >= deadline
404
+
405
+ deadline - now
406
+ end
317
407
  end
318
408
  end
@@ -108,7 +108,7 @@ module Excon
108
108
  end
109
109
 
110
110
  if @data[:proxy]
111
- request = "CONNECT #{@data[:host]}#{port_string(@data.merge(:omit_default_port => false))}#{Excon::HTTP_1_1}" +
111
+ request = "CONNECT #{@data[:host]}#{Excon::HTTP_1_1}" \
112
112
  "Host: #{@data[:host]}#{port_string(@data)}#{Excon::CR_NL}"
113
113
 
114
114
  if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
@@ -42,7 +42,6 @@ module Excon
42
42
  if RUBY_PLATFORM == 'java'
43
43
  @pid, @write, @read, @error = IO.popen4(*args)
44
44
  else
45
- GC.disable if RUBY_VERSION < '1.9'
46
45
  @pid, @write, @read, @error = Open4.popen4(*args)
47
46
  end
48
47
  @started_at = Time.now
@@ -57,7 +56,6 @@ module Excon
57
56
  Process.kill('USR1', pid)
58
57
  else
59
58
  Process.kill(9, pid)
60
- GC.enable if RUBY_VERSION < '1.9'
61
59
  Process.wait(pid)
62
60
  end
63
61