http 5.0.0.pre3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) 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.yml +7 -124
  6. data/.rubocop/layout.yml +8 -0
  7. data/.rubocop/style.yml +32 -0
  8. data/.rubocop_todo.yml +192 -0
  9. data/CHANGES.md +48 -1
  10. data/Gemfile +18 -11
  11. data/README.md +13 -17
  12. data/Rakefile +2 -10
  13. data/http.gemspec +2 -2
  14. data/lib/http/chainable.rb +15 -14
  15. data/lib/http/client.rb +18 -11
  16. data/lib/http/connection.rb +7 -3
  17. data/lib/http/content_type.rb +10 -5
  18. data/lib/http/feature.rb +1 -1
  19. data/lib/http/features/instrumentation.rb +1 -1
  20. data/lib/http/features/logging.rb +19 -21
  21. data/lib/http/headers.rb +3 -3
  22. data/lib/http/mime_type/adapter.rb +2 -0
  23. data/lib/http/options.rb +2 -2
  24. data/lib/http/redirector.rb +1 -1
  25. data/lib/http/request.rb +7 -4
  26. data/lib/http/response/body.rb +1 -2
  27. data/lib/http/response/inflater.rb +1 -1
  28. data/lib/http/response/parser.rb +72 -64
  29. data/lib/http/response/status.rb +2 -2
  30. data/lib/http/timeout/global.rb +16 -28
  31. data/lib/http/timeout/null.rb +2 -1
  32. data/lib/http/timeout/per_operation.rb +31 -55
  33. data/lib/http/version.rb +1 -1
  34. data/spec/lib/http/client_spec.rb +75 -41
  35. data/spec/lib/http/features/instrumentation_spec.rb +21 -15
  36. data/spec/lib/http/features/logging_spec.rb +2 -4
  37. data/spec/lib/http/headers_spec.rb +3 -3
  38. data/spec/lib/http/response/parser_spec.rb +2 -2
  39. data/spec/lib/http_spec.rb +20 -2
  40. data/spec/spec_helper.rb +21 -21
  41. data/spec/support/black_hole.rb +1 -1
  42. data/spec/support/dummy_server.rb +1 -1
  43. data/spec/support/dummy_server/servlet.rb +14 -2
  44. data/spec/support/fuubar.rb +21 -0
  45. data/spec/support/simplecov.rb +19 -0
  46. metadata +21 -16
  47. data/.coveralls.yml +0 -1
  48. data/.travis.yml +0 -38
data/lib/http/feature.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HTTP
4
4
  class Feature
5
- def initialize(opts = {}) # rubocop:disable Style/OptionHash
5
+ def initialize(opts = {})
6
6
  @opts = opts
7
7
  end
8
8
 
@@ -29,7 +29,7 @@ module HTTP
29
29
  def wrap_request(request)
30
30
  # Emit a separate "start" event, so a logger can print the request
31
31
  # being run without waiting for a response
32
- instrumenter.instrument("start_#{name}", :request => request) {}
32
+ instrumenter.instrument("start_#{name}", :request => request)
33
33
  instrumenter.start(name, :request => request)
34
34
  request
35
35
  end
@@ -9,6 +9,20 @@ module HTTP
9
9
  # HTTP.use(logging: {logger: Logger.new(STDOUT)}).get("https://example.com/")
10
10
  #
11
11
  class Logging < Feature
12
+ HTTP::Options.register_feature(:logging, self)
13
+
14
+ class NullLogger
15
+ %w[fatal error warn info debug].each do |level|
16
+ define_method(level.to_sym) do |*_args|
17
+ nil
18
+ end
19
+
20
+ define_method(:"#{level}?") do
21
+ true
22
+ end
23
+ end
24
+ end
25
+
12
26
  attr_reader :logger
13
27
 
14
28
  def initialize(logger: NullLogger.new)
@@ -17,39 +31,23 @@ module HTTP
17
31
 
18
32
  def wrap_request(request)
19
33
  logger.info { "> #{request.verb.to_s.upcase} #{request.uri}" }
20
- logger.debug do
21
- headers = request.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
22
- body = request.body.source
34
+ logger.debug { "#{stringify_headers(request.headers)}\n\n#{request.body.source}" }
23
35
 
24
- headers + "\n\n" + body.to_s
25
- end
26
36
  request
27
37
  end
28
38
 
29
39
  def wrap_response(response)
30
40
  logger.info { "< #{response.status}" }
31
- logger.debug do
32
- headers = response.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
33
- body = response.body.to_s
41
+ logger.debug { "#{stringify_headers(response.headers)}\n\n#{response.body}" }
34
42
 
35
- headers + "\n\n" + body
36
- end
37
43
  response
38
44
  end
39
45
 
40
- class NullLogger
41
- %w[fatal error warn info debug].each do |level|
42
- define_method(level.to_sym) do |*_args|
43
- nil
44
- end
46
+ private
45
47
 
46
- define_method(:"#{level}?") do
47
- true
48
- end
49
- end
48
+ def stringify_headers(headers)
49
+ headers.map { |name, value| "#{name}: #{value}" }.join("\n")
50
50
  end
51
-
52
- HTTP::Options.register_feature(:logging, self)
53
51
  end
54
52
  end
55
53
  end
data/lib/http/headers.rb CHANGED
@@ -17,7 +17,7 @@ module HTTP
17
17
 
18
18
  # Matches valid header field name according to RFC.
19
19
  # @see http://tools.ietf.org/html/rfc7230#section-3.2
20
- COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#\$%&'*+\-.^_`|~]+\z/.freeze
20
+ COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/.freeze
21
21
 
22
22
  # Class constructor.
23
23
  def initialize
@@ -111,7 +111,7 @@ module HTTP
111
111
  #
112
112
  # @return [Hash]
113
113
  def to_h
114
- Hash[keys.map { |k| [k, self[k]] }]
114
+ keys.map { |k| [k, self[k]] }.to_h
115
115
  end
116
116
  alias to_hash to_h
117
117
 
@@ -164,7 +164,7 @@ module HTTP
164
164
 
165
165
  # @!method hash
166
166
  # Compute a hash-code for this headers container.
167
- # Two conatiners with the same content will have the same hash code.
167
+ # Two containers with the same content will have the same hash code.
168
168
  #
169
169
  # @see http://www.ruby-doc.org/core/Object.html#method-i-hash
170
170
  # @return [Fixnum]
@@ -14,6 +14,7 @@ module HTTP
14
14
  def_delegators :instance, :encode, :decode
15
15
  end
16
16
 
17
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
17
18
  %w[encode decode].each do |operation|
18
19
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
20
  def #{operation}(*)
@@ -21,6 +22,7 @@ module HTTP
21
22
  end
22
23
  RUBY
23
24
  end
25
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
24
26
  end
25
27
  end
26
28
  end
data/lib/http/options.rb CHANGED
@@ -32,7 +32,7 @@ module HTTP
32
32
 
33
33
  def def_option(name, reader_only: false, &interpreter)
34
34
  defined_options << name.to_sym
35
- interpreter ||= lambda { |v| v }
35
+ interpreter ||= ->(v) { v }
36
36
 
37
37
  if reader_only
38
38
  attr_reader name
@@ -47,7 +47,7 @@ module HTTP
47
47
  end
48
48
  end
49
49
 
50
- def initialize(options = {}) # rubocop:disable Style/OptionHash
50
+ def initialize(options = {})
51
51
  defaults = {
52
52
  :response => :auto,
53
53
  :proxy => {},
@@ -39,7 +39,7 @@ module HTTP
39
39
  # @param [Hash] opts
40
40
  # @option opts [Boolean] :strict (true) redirector hops policy
41
41
  # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
42
- def initialize(opts = {}) # rubocop:disable Style/OptionHash
42
+ def initialize(opts = {})
43
43
  @strict = opts.fetch(:strict, true)
44
44
  @max_hops = opts.fetch(:max_hops, 5).to_i
45
45
  end
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
@@ -213,7 +216,7 @@ module HTTP
213
216
 
214
217
  # @return [String] Default host (with port if needed) header value.
215
218
  def default_host_header_value
216
- PORTS[@scheme] != port ? "#{host}:#{port}" : host
219
+ PORTS[@scheme] == port ? host : "#{host}:#{port}"
217
220
  end
218
221
 
219
222
  def prepare_body(body)
@@ -223,8 +226,8 @@ module HTTP
223
226
  def prepare_headers(headers)
224
227
  headers = HTTP::Headers.coerce(headers || {})
225
228
 
226
- headers[Headers::HOST] ||= default_host_header_value
227
- headers[Headers::USER_AGENT] ||= USER_AGENT
229
+ headers[Headers::HOST] ||= default_host_header_value
230
+ headers[Headers::USER_AGENT] ||= USER_AGENT
228
231
 
229
232
  headers
230
233
  end
@@ -27,8 +27,7 @@ module HTTP
27
27
  # (see HTTP::Client#readpartial)
28
28
  def readpartial(*args)
29
29
  stream!
30
- chunk = @stream.readpartial(*args)
31
- chunk.force_encoding(@encoding) if chunk
30
+ @stream.readpartial(*args)&.force_encoding(@encoding)
32
31
  end
33
32
 
34
33
  # Iterate over the body, allowing it to be enumerable
@@ -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.finish
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
@@ -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