http 3.3.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +3 -1
  4. data/.travis.yml +10 -7
  5. data/CHANGES.md +135 -0
  6. data/README.md +14 -10
  7. data/Rakefile +1 -1
  8. data/http.gemspec +12 -5
  9. data/lib/http.rb +1 -2
  10. data/lib/http/chainable.rb +20 -29
  11. data/lib/http/client.rb +25 -19
  12. data/lib/http/connection.rb +5 -9
  13. data/lib/http/feature.rb +14 -0
  14. data/lib/http/features/auto_deflate.rb +27 -6
  15. data/lib/http/features/auto_inflate.rb +33 -6
  16. data/lib/http/features/instrumentation.rb +64 -0
  17. data/lib/http/features/logging.rb +55 -0
  18. data/lib/http/features/normalize_uri.rb +17 -0
  19. data/lib/http/headers/known.rb +3 -0
  20. data/lib/http/options.rb +27 -21
  21. data/lib/http/redirector.rb +2 -1
  22. data/lib/http/request.rb +38 -30
  23. data/lib/http/request/body.rb +30 -1
  24. data/lib/http/request/writer.rb +21 -7
  25. data/lib/http/response.rb +7 -15
  26. data/lib/http/response/parser.rb +56 -16
  27. data/lib/http/timeout/global.rb +12 -14
  28. data/lib/http/timeout/per_operation.rb +5 -7
  29. data/lib/http/uri.rb +13 -0
  30. data/lib/http/version.rb +1 -1
  31. data/spec/lib/http/client_spec.rb +34 -7
  32. data/spec/lib/http/features/auto_inflate_spec.rb +38 -22
  33. data/spec/lib/http/features/instrumentation_spec.rb +56 -0
  34. data/spec/lib/http/features/logging_spec.rb +67 -0
  35. data/spec/lib/http/redirector_spec.rb +13 -0
  36. data/spec/lib/http/request/body_spec.rb +51 -0
  37. data/spec/lib/http/request/writer_spec.rb +20 -0
  38. data/spec/lib/http/request_spec.rb +6 -0
  39. data/spec/lib/http/response/parser_spec.rb +45 -0
  40. data/spec/lib/http/response_spec.rb +3 -4
  41. data/spec/lib/http_spec.rb +45 -65
  42. data/spec/regression_specs.rb +7 -0
  43. data/spec/support/dummy_server/servlet.rb +5 -0
  44. data/spec/support/http_handling_shared.rb +60 -64
  45. metadata +32 -21
  46. data/.ruby-version +0 -1
data/lib/http/request.rb CHANGED
@@ -66,6 +66,8 @@ module HTTP
66
66
  # Scheme is normalized to be a lowercase symbol e.g. :http, :https
67
67
  attr_reader :scheme
68
68
 
69
+ attr_reader :uri_normalizer
70
+
69
71
  # "Request URI" as per RFC 2616
70
72
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
71
73
  attr_reader :uri
@@ -73,25 +75,25 @@ module HTTP
73
75
 
74
76
  # @option opts [String] :version
75
77
  # @option opts [#to_s] :verb HTTP request method
78
+ # @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER)
76
79
  # @option opts [HTTP::URI, #to_s] :uri
77
80
  # @option opts [Hash] :headers
78
81
  # @option opts [Hash] :proxy
79
82
  # @option opts [String, Enumerable, IO, nil] :body
80
83
  def initialize(opts)
81
- @verb = opts.fetch(:verb).to_s.downcase.to_sym
82
- @uri = normalize_uri(opts.fetch(:uri))
84
+ @verb = opts.fetch(:verb).to_s.downcase.to_sym
85
+ @uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER
86
+
87
+ @uri = @uri_normalizer.call(opts.fetch(:uri))
83
88
  @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
84
89
 
85
90
  raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
86
91
  raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
87
92
 
88
93
  @proxy = opts[:proxy] || {}
89
- @body = request_body(opts[:body], opts)
90
94
  @version = opts[:version] || "1.1"
91
- @headers = HTTP::Headers.coerce(opts[:headers] || {})
92
-
93
- @headers[Headers::HOST] ||= default_host_header_value
94
- @headers[Headers::USER_AGENT] ||= USER_AGENT
95
+ @headers = prepare_headers(opts[:headers])
96
+ @body = prepare_body(opts[:body])
95
97
  end
96
98
 
97
99
  # Returns new Request with updated uri
@@ -100,12 +102,13 @@ module HTTP
100
102
  headers.delete(Headers::HOST)
101
103
 
102
104
  self.class.new(
103
- :verb => verb,
104
- :uri => @uri.join(uri),
105
- :headers => headers,
106
- :proxy => proxy,
107
- :body => body,
108
- :version => version
105
+ :verb => verb,
106
+ :uri => @uri.join(uri),
107
+ :headers => headers,
108
+ :proxy => proxy,
109
+ :body => body.source,
110
+ :version => version,
111
+ :uri_normalizer => uri_normalizer
109
112
  )
110
113
  end
111
114
 
@@ -184,15 +187,20 @@ module HTTP
184
187
  using_proxy? ? proxy[:proxy_port] : port
185
188
  end
186
189
 
187
- private
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
190
+ # Human-readable representation of base request info.
191
+ #
192
+ # @example
193
+ #
194
+ # req.inspect
195
+ # # => #<HTTP::Request/1.1 GET https://example.com>
196
+ #
197
+ # @return [String]
198
+ def inspect
199
+ "#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>"
194
200
  end
195
201
 
202
+ private
203
+
196
204
  # @!attribute [r] host
197
205
  # @return [String]
198
206
  def_delegator :@uri, :host
@@ -208,17 +216,17 @@ module HTTP
208
216
  PORTS[@scheme] != port ? "#{host}:#{port}" : host
209
217
  end
210
218
 
211
- # @return [HTTP::URI] URI with all componentes but query being normalized.
212
- def normalize_uri(uri)
213
- uri = HTTP::URI.parse uri
219
+ def prepare_body(body)
220
+ body.is_a?(Request::Body) ? body : Request::Body.new(body)
221
+ end
214
222
 
215
- HTTP::URI.new(
216
- :scheme => uri.normalized_scheme,
217
- :authority => uri.normalized_authority,
218
- :path => uri.normalized_path,
219
- :query => uri.query,
220
- :fragment => uri.normalized_fragment
221
- )
223
+ def prepare_headers(headers)
224
+ headers = HTTP::Headers.coerce(headers || {})
225
+
226
+ headers[Headers::HOST] ||= default_host_header_value
227
+ headers[Headers::USER_AGENT] ||= USER_AGENT
228
+
229
+ headers
222
230
  end
223
231
  end
224
232
  end
@@ -35,14 +35,43 @@ module HTTP
35
35
  yield @source
36
36
  elsif @source.respond_to?(:read)
37
37
  IO.copy_stream(@source, ProcIO.new(block))
38
- @source.rewind if @source.respond_to?(:rewind)
38
+ rewind(@source)
39
39
  elsif @source.is_a?(Enumerable)
40
40
  @source.each(&block)
41
41
  end
42
+
43
+ self
44
+ end
45
+
46
+ # Request bodies are equivalent when they have the same source.
47
+ def ==(other)
48
+ self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
42
49
  end
43
50
 
44
51
  private
45
52
 
53
+ def rewind(io)
54
+ io.rewind if io.respond_to? :rewind
55
+ rescue Errno::ESPIPE, Errno::EPIPE
56
+ # Pipe IOs respond to `:rewind` but fail when you call it.
57
+ #
58
+ # Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
59
+ # but *EPIPE* on jRuby.
60
+ #
61
+ # - **ESPIPE** -- "Illegal seek."
62
+ # Invalid seek operation (such as on a pipe).
63
+ #
64
+ # - **EPIPE** -- "Broken pipe."
65
+ # There is no process reading from the other end of a pipe. Every
66
+ # library function that returns this error code also generates
67
+ # a SIGPIPE signal; this signal terminates the program if not handled
68
+ # or blocked. Thus, your program will never actually see EPIPE unless
69
+ # it has handled or blocked SIGPIPE.
70
+ #
71
+ # See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
72
+ nil
73
+ end
74
+
46
75
  def validate_source_type!
47
76
  return if @source.is_a?(String)
48
77
  return if @source.respond_to?(:read)
@@ -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
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?(: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
@@ -1,41 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "http-parser"
4
+
3
5
  module HTTP
4
6
  class Response
7
+ # @api private
8
+ #
9
+ # NOTE(ixti): This class is a subject of future refactoring, thus don't
10
+ # expect this class API to be stable until this message disappears and
11
+ # class is not marked as private anymore.
5
12
  class Parser
6
13
  attr_reader :headers
7
14
 
8
15
  def initialize
9
- @parser = HTTP::Parser.new(self)
16
+ @state = HttpParser::Parser.new_instance { |i| i.type = :response }
17
+ @parser = HttpParser::Parser.new(self)
18
+
10
19
  reset
11
20
  end
12
21
 
22
+ # @return [self]
13
23
  def add(data)
14
- @parser << data
24
+ # XXX(ixti): API doc of HttpParser::Parser is misleading, it says that
25
+ # it returns boolean true if data was parsed successfully, but instead
26
+ # it's response tells if there was an error; So when it's `true` that
27
+ # means parse failed, and `false` means parse was successful.
28
+ # case of success.
29
+ return self unless @parser.parse(@state, data)
30
+
31
+ raise IOError, "Could not parse data"
15
32
  end
16
33
  alias << add
17
34
 
18
35
  def headers?
19
- !!@headers
36
+ @finished[:headers]
20
37
  end
21
38
 
22
39
  def http_version
23
- @parser.http_version.join(".")
40
+ @state.http_version
24
41
  end
25
42
 
26
43
  def status_code
27
- @parser.status_code
44
+ @state.http_status
28
45
  end
29
46
 
30
47
  #
31
48
  # HTTP::Parser callbacks
32
49
  #
33
50
 
34
- def on_headers_complete(headers)
35
- @headers = headers
51
+ def on_header_field(_response, field)
52
+ append_header if @reading_header_value
53
+ @field << field
54
+ end
55
+
56
+ def on_header_value(_response, value)
57
+ @reading_header_value = true
58
+ @field_value << value
59
+ end
60
+
61
+ def on_headers_complete(_reposse)
62
+ append_header if @reading_header_value
63
+ @finished[:headers] = true
36
64
  end
37
65
 
38
- def on_body(chunk)
66
+ def on_body(_response, chunk)
39
67
  if @chunk
40
68
  @chunk << chunk
41
69
  else
@@ -57,20 +85,32 @@ module HTTP
57
85
  chunk
58
86
  end
59
87
 
60
- def on_message_complete
61
- @finished = true
88
+ def on_message_complete(_response)
89
+ @finished[:message] = true
62
90
  end
63
91
 
64
92
  def reset
65
- @parser.reset!
66
-
67
- @finished = false
68
- @headers = nil
69
- @chunk = nil
93
+ @state.reset!
94
+
95
+ @finished = Hash.new(false)
96
+ @headers = HTTP::Headers.new
97
+ @reading_header_value = false
98
+ @field = +""
99
+ @field_value = +""
100
+ @chunk = nil
70
101
  end
71
102
 
72
103
  def finished?
73
- @finished
104
+ @finished[:message]
105
+ end
106
+
107
+ private
108
+
109
+ def append_header
110
+ @headers.add(@field, @field_value)
111
+ @reading_header_value = false
112
+ @field_value = +""
113
+ @field = +""
74
114
  end
75
115
  end
76
116
  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