http 5.0.0.pre3 → 5.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +65 -0
  3. data/.gitignore +6 -10
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/style.yml +32 -0
  7. data/.rubocop.yml +7 -124
  8. data/.rubocop_todo.yml +192 -0
  9. data/CHANGES.md +123 -1
  10. data/Gemfile +18 -11
  11. data/LICENSE.txt +1 -1
  12. data/README.md +47 -87
  13. data/Rakefile +2 -10
  14. data/http.gemspec +3 -3
  15. data/lib/http/chainable.rb +15 -14
  16. data/lib/http/client.rb +26 -15
  17. data/lib/http/connection.rb +7 -3
  18. data/lib/http/content_type.rb +10 -5
  19. data/lib/http/feature.rb +1 -1
  20. data/lib/http/features/auto_inflate.rb +0 -2
  21. data/lib/http/features/instrumentation.rb +1 -1
  22. data/lib/http/features/logging.rb +19 -21
  23. data/lib/http/headers.rb +3 -3
  24. data/lib/http/mime_type/adapter.rb +2 -0
  25. data/lib/http/options.rb +2 -2
  26. data/lib/http/redirector.rb +1 -1
  27. data/lib/http/request/writer.rb +6 -2
  28. data/lib/http/request.rb +22 -5
  29. data/lib/http/response/body.rb +5 -4
  30. data/lib/http/response/inflater.rb +1 -1
  31. data/lib/http/response/parser.rb +72 -64
  32. data/lib/http/response/status.rb +2 -2
  33. data/lib/http/response.rb +24 -6
  34. data/lib/http/timeout/global.rb +18 -30
  35. data/lib/http/timeout/null.rb +2 -1
  36. data/lib/http/timeout/per_operation.rb +31 -55
  37. data/lib/http/version.rb +1 -1
  38. data/spec/lib/http/client_spec.rb +109 -41
  39. data/spec/lib/http/features/auto_inflate_spec.rb +0 -1
  40. data/spec/lib/http/features/instrumentation_spec.rb +21 -16
  41. data/spec/lib/http/features/logging_spec.rb +2 -5
  42. data/spec/lib/http/headers_spec.rb +3 -3
  43. data/spec/lib/http/redirector_spec.rb +44 -0
  44. data/spec/lib/http/request/body_spec.rb +3 -3
  45. data/spec/lib/http/request/writer_spec.rb +12 -1
  46. data/spec/lib/http/response/body_spec.rb +5 -5
  47. data/spec/lib/http/response/parser_spec.rb +5 -5
  48. data/spec/lib/http/response_spec.rb +62 -10
  49. data/spec/lib/http_spec.rb +20 -2
  50. data/spec/spec_helper.rb +21 -21
  51. data/spec/support/black_hole.rb +1 -1
  52. data/spec/support/dummy_server/servlet.rb +14 -2
  53. data/spec/support/dummy_server.rb +1 -1
  54. data/spec/support/fuubar.rb +21 -0
  55. data/spec/support/simplecov.rb +19 -0
  56. metadata +23 -18
  57. data/.coveralls.yml +0 -1
  58. data/.travis.yml +0 -38
@@ -47,7 +47,11 @@ module HTTP
47
47
  # Adds the headers to the header array for the given request body we are working
48
48
  # with
49
49
  def add_body_type_headers
50
- return if @headers[Headers::CONTENT_LENGTH] || chunked?
50
+ return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
51
+ @body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
52
+ @request_header[0].start_with?("#{method} ")
53
+ end
54
+ )
51
55
 
52
56
  @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
53
57
  end
@@ -57,7 +61,7 @@ module HTTP
57
61
  def join_headers
58
62
  # join the headers array with crlfs, stick two on the end because
59
63
  # that ends the request header
60
- @request_header.join(CRLF) + CRLF * 2
64
+ @request_header.join(CRLF) + (CRLF * 2)
61
65
  end
62
66
 
63
67
  # Writes HTTP request data into the socket.
data/lib/http/request.rb CHANGED
@@ -46,7 +46,10 @@ module HTTP
46
46
  :patch,
47
47
 
48
48
  # draft-reschke-webdav-search: WebDAV Search
49
- :search
49
+ :search,
50
+
51
+ # RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
52
+ :mkcalendar
50
53
  ].freeze
51
54
 
52
55
  # Allowed schemes
@@ -101,12 +104,26 @@ module HTTP
101
104
  headers = self.headers.dup
102
105
  headers.delete(Headers::HOST)
103
106
 
107
+ new_body = body.source
108
+ if verb == :get
109
+ # request bodies should not always be resubmitted when following a redirect
110
+ # some servers will close the connection after receiving the request headers
111
+ # which may cause Errno::ECONNRESET: Connection reset by peer
112
+ # see https://github.com/httprb/http/issues/649
113
+ # new_body = Request::Body.new(nil)
114
+ new_body = nil
115
+ # the CONTENT_TYPE header causes problems if set on a get request w/ an empty body
116
+ # the server might assume that there should be content if it is set to multipart
117
+ # rack raises EmptyContentError if this happens
118
+ headers.delete(Headers::CONTENT_TYPE)
119
+ end
120
+
104
121
  self.class.new(
105
122
  :verb => verb,
106
123
  :uri => @uri.join(uri),
107
124
  :headers => headers,
108
125
  :proxy => proxy,
109
- :body => body.source,
126
+ :body => new_body,
110
127
  :version => version,
111
128
  :uri_normalizer => uri_normalizer
112
129
  )
@@ -213,7 +230,7 @@ module HTTP
213
230
 
214
231
  # @return [String] Default host (with port if needed) header value.
215
232
  def default_host_header_value
216
- PORTS[@scheme] != port ? "#{host}:#{port}" : host
233
+ PORTS[@scheme] == port ? host : "#{host}:#{port}"
217
234
  end
218
235
 
219
236
  def prepare_body(body)
@@ -223,8 +240,8 @@ module HTTP
223
240
  def prepare_headers(headers)
224
241
  headers = HTTP::Headers.coerce(headers || {})
225
242
 
226
- headers[Headers::HOST] ||= default_host_header_value
227
- headers[Headers::USER_AGENT] ||= USER_AGENT
243
+ headers[Headers::HOST] ||= default_host_header_value
244
+ headers[Headers::USER_AGENT] ||= USER_AGENT
228
245
 
229
246
  headers
230
247
  end
@@ -28,7 +28,8 @@ module HTTP
28
28
  def readpartial(*args)
29
29
  stream!
30
30
  chunk = @stream.readpartial(*args)
31
- chunk.force_encoding(@encoding) if chunk
31
+
32
+ String.new(chunk, :encoding => @encoding) if chunk
32
33
  end
33
34
 
34
35
  # Iterate over the body, allowing it to be enumerable
@@ -46,11 +47,11 @@ module HTTP
46
47
 
47
48
  begin
48
49
  @streaming = false
49
- @contents = String.new("").force_encoding(@encoding)
50
+ @contents = String.new("", :encoding => @encoding)
50
51
 
51
52
  while (chunk = @stream.readpartial)
52
- @contents << chunk.force_encoding(@encoding)
53
- chunk.clear # deallocate string
53
+ @contents << String.new(chunk, :encoding => @encoding)
54
+ chunk = nil # deallocate string
54
55
  end
55
56
  rescue
56
57
  @contents = nil
@@ -16,7 +16,7 @@ module HTTP
16
16
  if chunk
17
17
  chunk = zstream.inflate(chunk)
18
18
  elsif !zstream.closed?
19
- zstream.finish
19
+ zstream.finish if zstream.total_in.positive?
20
20
  zstream.close
21
21
  end
22
22
  chunk
@@ -1,69 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "http-parser"
3
+ require "llhttp"
4
4
 
5
5
  module HTTP
6
6
  class Response
7
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.
12
8
  class Parser
13
- attr_reader :headers
9
+ attr_reader :parser, :headers, :status_code, :http_version
14
10
 
15
11
  def initialize
16
- @state = HttpParser::Parser.new_instance { |i| i.type = :response }
17
- @parser = HttpParser::Parser.new(self)
18
-
12
+ @handler = Handler.new(self)
13
+ @parser = LLHttp::Parser.new(@handler, :type => :response)
19
14
  reset
20
15
  end
21
16
 
22
- # @return [self]
23
- def add(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"
17
+ def reset
18
+ @parser.reset
19
+ @handler.reset
20
+ @header_finished = false
21
+ @message_finished = false
22
+ @headers = Headers.new
23
+ @chunk = nil
24
+ @status_code = nil
25
+ @http_version = nil
32
26
  end
33
- alias << add
34
27
 
35
- def headers?
36
- @finished[:headers]
37
- end
28
+ def add(data)
29
+ parser << data
38
30
 
39
- def http_version
40
- @state.http_version
31
+ self
32
+ rescue LLHttp::Error => e
33
+ raise IOError, e.message
41
34
  end
42
35
 
43
- def status_code
44
- @state.http_status
36
+ alias << add
37
+
38
+ def mark_header_finished
39
+ @header_finished = true
40
+ @status_code = @parser.status_code
41
+ @http_version = "#{@parser.http_major}.#{@parser.http_minor}"
45
42
  end
46
43
 
47
- #
48
- # HTTP::Parser callbacks
49
- #
44
+ def headers?
45
+ @header_finished
46
+ end
50
47
 
51
- def on_header_field(_response, field)
52
- append_header if @reading_header_value
53
- @field << field
48
+ def add_header(name, value)
49
+ @headers.add(name, value)
54
50
  end
55
51
 
56
- def on_header_value(_response, value)
57
- @reading_header_value = true
58
- @field_value << value
52
+ def mark_message_finished
53
+ @message_finished = true
59
54
  end
60
55
 
61
- def on_headers_complete(_reposse)
62
- append_header if @reading_header_value
63
- @finished[:headers] = true
56
+ def finished?
57
+ @message_finished
64
58
  end
65
59
 
66
- def on_body(_response, chunk)
60
+ def add_body(chunk)
67
61
  if @chunk
68
62
  @chunk << chunk
69
63
  else
@@ -85,36 +79,50 @@ module HTTP
85
79
  chunk
86
80
  end
87
81
 
88
- def on_message_complete(_response)
89
- if @state.http_status < 200
82
+ class Handler < LLHttp::Delegate
83
+ def initialize(target)
84
+ @target = target
85
+ super()
90
86
  reset
91
- else
92
- @finished[:message] = true
93
87
  end
94
- end
95
88
 
96
- def reset
97
- @state.reset!
98
-
99
- @finished = Hash.new(false)
100
- @headers = HTTP::Headers.new
101
- @reading_header_value = false
102
- @field = +""
103
- @field_value = +""
104
- @chunk = nil
105
- end
89
+ def reset
90
+ @reading_header_value = false
91
+ @field_value = +""
92
+ @field = +""
93
+ end
106
94
 
107
- def finished?
108
- @finished[:message]
109
- end
95
+ def on_header_field(field)
96
+ append_header if @reading_header_value
97
+ @field << field
98
+ end
99
+
100
+ def on_header_value(value)
101
+ @reading_header_value = true
102
+ @field_value << value
103
+ end
110
104
 
111
- private
105
+ def on_headers_complete
106
+ append_header if @reading_header_value
107
+ @target.mark_header_finished
108
+ end
109
+
110
+ def on_body(body)
111
+ @target.add_body(body)
112
+ end
112
113
 
113
- def append_header
114
- @headers.add(@field, @field_value)
115
- @reading_header_value = false
116
- @field_value = +""
117
- @field = +""
114
+ def on_message_complete
115
+ @target.mark_message_finished
116
+ end
117
+
118
+ private
119
+
120
+ def append_header
121
+ @target.add_header(@field, @field_value)
122
+ @reading_header_value = false
123
+ @field_value = +""
124
+ @field = +""
125
+ end
118
126
  end
119
127
  end
120
128
  end
@@ -58,7 +58,7 @@ module HTTP
58
58
  # SYMBOLS[418] # => :im_a_teapot
59
59
  #
60
60
  # @return [Hash<Fixnum => Symbol>]
61
- SYMBOLS = Hash[REASONS.map { |k, v| [k, symbolize(v)] }].freeze
61
+ SYMBOLS = REASONS.transform_values { |v| symbolize(v) }.freeze
62
62
 
63
63
  # Reversed {SYMBOLS} map.
64
64
  #
@@ -69,7 +69,7 @@ module HTTP
69
69
  # SYMBOL_CODES[:im_a_teapot] # => 418
70
70
  #
71
71
  # @return [Hash<Symbol => Fixnum>]
72
- SYMBOL_CODES = Hash[SYMBOLS.map { |k, v| [v, k] }].freeze
72
+ SYMBOL_CODES = SYMBOLS.map { |k, v| [v, k] }.to_h.freeze
73
73
 
74
74
  # @return [Fixnum] status code
75
75
  attr_reader :code
data/lib/http/response.rb CHANGED
@@ -40,10 +40,11 @@ module HTTP
40
40
  # @option opts [HTTP::Connection] :connection
41
41
  # @option opts [String] :encoding Encoding to use when reading body
42
42
  # @option opts [String] :body
43
- # @option opts [HTTP::Request] request
43
+ # @option opts [HTTP::Request] request The request this is in response to.
44
+ # @option opts [String] :uri (DEPRECATED) used to populate a missing request
44
45
  def initialize(opts)
45
46
  @version = opts.fetch(:version)
46
- @request = opts.fetch(:request)
47
+ @request = init_request(opts)
47
48
  @status = HTTP::Response::Status.new(opts.fetch(:status))
48
49
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
49
50
  @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
@@ -137,8 +138,8 @@ module HTTP
137
138
  def_delegator :content_type, :charset
138
139
 
139
140
  def cookies
140
- @cookies ||= headers.each_with_object CookieJar.new do |(k, v), jar|
141
- jar.parse(v, uri) if k == Headers::SET_COOKIE
141
+ @cookies ||= headers.get(Headers::SET_COOKIE).each_with_object CookieJar.new do |v, jar|
142
+ jar.parse(v, uri)
142
143
  end
143
144
  end
144
145
 
@@ -156,13 +157,30 @@ module HTTP
156
157
  # @param type [#to_s] Parse as given MIME type.
157
158
  # @raise (see MimeType.[])
158
159
  # @return [Object]
159
- def parse(type)
160
- MimeType[type].decode to_s
160
+ def parse(type = nil)
161
+ MimeType[type || mime_type].decode to_s
161
162
  end
162
163
 
163
164
  # Inspect a response
164
165
  def inspect
165
166
  "#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
166
167
  end
168
+
169
+ private
170
+
171
+ # Initialize an HTTP::Request from options.
172
+ #
173
+ # @return [HTTP::Request]
174
+ def init_request(opts)
175
+ raise ArgumentError, ":uri is for backwards compatibilty and conflicts with :request" \
176
+ if opts[:request] && opts[:uri]
177
+
178
+ # For backwards compatibilty
179
+ if opts[:uri]
180
+ HTTP::Request.new(:uri => opts[:uri], :verb => :get)
181
+ else
182
+ opts.fetch(:request)
183
+ end
184
+ end
167
185
  end
168
186
  end
@@ -35,11 +35,11 @@ module HTTP
35
35
  begin
36
36
  @socket.connect_nonblock
37
37
  rescue IO::WaitReadable
38
- IO.select([@socket], nil, nil, @time_left)
38
+ @socket.wait_readable(@time_left)
39
39
  log_time
40
40
  retry
41
41
  rescue IO::WaitWritable
42
- IO.select(nil, [@socket], nil, @time_left)
42
+ @socket.wait_writable(@time_left)
43
43
  log_time
44
44
  retry
45
45
  end
@@ -59,22 +59,12 @@ module HTTP
59
59
 
60
60
  private
61
61
 
62
- if RUBY_VERSION < "2.1.0"
63
- def read_nonblock(size, buffer = nil)
64
- @socket.read_nonblock(size, buffer)
65
- end
66
-
67
- def write_nonblock(data)
68
- @socket.write_nonblock(data)
69
- end
70
- else
71
- def read_nonblock(size, buffer = nil)
72
- @socket.read_nonblock(size, buffer, :exception => false)
73
- end
62
+ def read_nonblock(size, buffer = nil)
63
+ @socket.read_nonblock(size, buffer, :exception => false)
64
+ end
74
65
 
75
- def write_nonblock(data)
76
- @socket.write_nonblock(data, :exception => false)
77
- end
66
+ def write_nonblock(data)
67
+ @socket.write_nonblock(data, :exception => false)
78
68
  end
79
69
 
80
70
  # Perform the given I/O operation with the given argument
@@ -82,20 +72,18 @@ module HTTP
82
72
  reset_timer
83
73
 
84
74
  loop do
85
- begin
86
- result = yield
87
-
88
- case result
89
- when :wait_readable then wait_readable_or_timeout
90
- when :wait_writable then wait_writable_or_timeout
91
- when NilClass then return :eof
92
- else return result
93
- end
94
- rescue IO::WaitReadable
95
- wait_readable_or_timeout
96
- rescue IO::WaitWritable
97
- wait_writable_or_timeout
75
+ result = yield
76
+
77
+ case result
78
+ when :wait_readable then wait_readable_or_timeout
79
+ when :wait_writable then wait_writable_or_timeout
80
+ when NilClass then return :eof
81
+ else return result
98
82
  end
83
+ rescue IO::WaitReadable
84
+ wait_readable_or_timeout
85
+ rescue IO::WaitWritable
86
+ wait_writable_or_timeout
99
87
  end
100
88
  rescue EOFError
101
89
  :eof
@@ -12,7 +12,7 @@ module HTTP
12
12
 
13
13
  attr_reader :options, :socket
14
14
 
15
- def initialize(options = {}) # rubocop:disable Style/OptionHash
15
+ def initialize(options = {})
16
16
  @options = options
17
17
  end
18
18
 
@@ -36,6 +36,7 @@ module HTTP
36
36
  connect_ssl
37
37
 
38
38
  return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
39
+ return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
39
40
 
40
41
  @socket.post_connection_check(host)
41
42
  end
@@ -34,66 +34,42 @@ module HTTP
34
34
  end
35
35
  end
36
36
 
37
- # NIO with exceptions
38
- if RUBY_VERSION < "2.1.0"
39
- # Read data from the socket
40
- def readpartial(size, buffer = nil)
41
- rescue_readable do
42
- @socket.read_nonblock(size, buffer)
43
- end
44
- rescue EOFError
45
- :eof
46
- end
47
-
48
- # Write data to the socket
49
- def write(data)
50
- rescue_writable do
51
- @socket.write_nonblock(data)
52
- end
53
- rescue EOFError
54
- :eof
55
- end
56
-
57
- # NIO without exceptions
58
- else
59
- # Read data from the socket
60
- def readpartial(size, buffer = nil)
61
- timeout = false
62
- loop do
63
- result = @socket.read_nonblock(size, buffer, :exception => false)
64
-
65
- return :eof if result.nil?
66
- return result if result != :wait_readable
67
-
68
- raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
69
-
70
- # marking the socket for timeout. Why is this not being raised immediately?
71
- # it seems there is some race-condition on the network level between calling
72
- # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
73
- # for reads, and when waiting for x seconds, it returns nil suddenly without completing
74
- # the x seconds. In a normal case this would be a timeout on wait/read, but it can
75
- # also mean that the socket has been closed by the server. Therefore we "mark" the
76
- # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
77
- # timeout. Else, the first timeout was a proper timeout.
78
- # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
79
- # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
80
- timeout = true unless @socket.to_io.wait_readable(@read_timeout)
81
- end
37
+ # Read data from the socket
38
+ def readpartial(size, buffer = nil)
39
+ timeout = false
40
+ loop do
41
+ result = @socket.read_nonblock(size, buffer, :exception => false)
42
+
43
+ return :eof if result.nil?
44
+ return result if result != :wait_readable
45
+
46
+ raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
47
+
48
+ # marking the socket for timeout. Why is this not being raised immediately?
49
+ # it seems there is some race-condition on the network level between calling
50
+ # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
51
+ # for reads, and when waiting for x seconds, it returns nil suddenly without completing
52
+ # the x seconds. In a normal case this would be a timeout on wait/read, but it can
53
+ # also mean that the socket has been closed by the server. Therefore we "mark" the
54
+ # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
55
+ # timeout. Else, the first timeout was a proper timeout.
56
+ # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
57
+ # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
58
+ timeout = true unless @socket.to_io.wait_readable(@read_timeout)
82
59
  end
60
+ end
83
61
 
84
- # Write data to the socket
85
- def write(data)
86
- timeout = false
87
- loop do
88
- result = @socket.write_nonblock(data, :exception => false)
89
- return result unless result == :wait_writable
62
+ # Write data to the socket
63
+ def write(data)
64
+ timeout = false
65
+ loop do
66
+ result = @socket.write_nonblock(data, :exception => false)
67
+ return result unless result == :wait_writable
90
68
 
91
- raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
69
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
92
70
 
93
- timeout = true unless @socket.to_io.wait_writable(@write_timeout)
94
- end
71
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
95
72
  end
96
-
97
73
  end
98
74
  end
99
75
  end
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.0.0.pre3"
4
+ VERSION = "5.0.3"
5
5
  end