http 5.0.0.pre2 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 +114 -1
  10. data/Gemfile +18 -11
  11. data/LICENSE.txt +1 -1
  12. data/README.md +13 -16
  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 +5 -1
  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 +74 -62
  32. data/lib/http/response/status.rb +2 -2
  33. data/lib/http/response.rb +22 -4
  34. data/lib/http/timeout/global.rb +41 -35
  35. data/lib/http/timeout/null.rb +2 -1
  36. data/lib/http/timeout/per_operation.rb +56 -59
  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/writer_spec.rb +12 -1
  45. data/spec/lib/http/response/body_spec.rb +5 -5
  46. data/spec/lib/http/response/parser_spec.rb +30 -1
  47. data/spec/lib/http/response_spec.rb +62 -10
  48. data/spec/lib/http_spec.rb +20 -2
  49. data/spec/spec_helper.rb +21 -21
  50. data/spec/support/black_hole.rb +1 -1
  51. data/spec/support/dummy_server/servlet.rb +14 -2
  52. data/spec/support/dummy_server.rb +1 -1
  53. data/spec/support/fuubar.rb +21 -0
  54. data/spec/support/simplecov.rb +19 -0
  55. metadata +23 -17
  56. data/.coveralls.yml +0 -1
  57. data/.travis.yml +0 -38
@@ -3,7 +3,6 @@
3
3
  require "forwardable"
4
4
 
5
5
  require "http/headers"
6
- require "http/response/parser"
7
6
 
8
7
  module HTTP
9
8
  # A connection to the HTTP server
@@ -68,8 +67,13 @@ module HTTP
68
67
  # @param [Request] req Request to send to the server
69
68
  # @return [nil]
70
69
  def send_request(req)
71
- raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body." if @pending_response
72
- raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body." if @pending_request
70
+ if @pending_response
71
+ raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
72
+ end
73
+
74
+ if @pending_request
75
+ raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
76
+ end
73
77
 
74
78
  @pending_request = true
75
79
 
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- ContentType = Struct.new(:mime_type, :charset) do
4
+ class ContentType
5
5
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
6
6
  CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
7
7
 
8
+ attr_accessor :mime_type, :charset
9
+
8
10
  class << self
9
11
  # Parse string and return ContentType struct
10
12
  def parse(str)
@@ -15,15 +17,18 @@ module HTTP
15
17
 
16
18
  # :nodoc:
17
19
  def mime_type(str)
18
- m = str.to_s[MIME_TYPE_RE, 1]
19
- m && m.strip.downcase
20
+ str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
20
21
  end
21
22
 
22
23
  # :nodoc:
23
24
  def charset(str)
24
- m = str.to_s[CHARSET_RE, 1]
25
- m && m.strip.delete('"')
25
+ str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
26
26
  end
27
27
  end
28
+
29
+ def initialize(mime_type = nil, charset = nil)
30
+ @mime_type = mime_type
31
+ @charset = charset
32
+ end
28
33
  end
29
34
  end
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
 
@@ -21,8 +21,6 @@ module HTTP
21
21
  :request => response.request
22
22
  }
23
23
 
24
- options[:uri] = response.uri if response.uri
25
-
26
24
  Response.new(options)
27
25
  end
28
26
 
@@ -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
@@ -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
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,32 +79,50 @@ module HTTP
85
79
  chunk
86
80
  end
87
81
 
88
- def on_message_complete(_response)
89
- @finished[:message] = true
90
- end
82
+ class Handler < LLHttp::Delegate
83
+ def initialize(target)
84
+ @target = target
85
+ super()
86
+ reset
87
+ end
91
88
 
92
- def reset
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
101
- end
89
+ def reset
90
+ @reading_header_value = false
91
+ @field_value = +""
92
+ @field = +""
93
+ end
102
94
 
103
- def finished?
104
- @finished[:message]
105
- 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
106
104
 
107
- 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
108
113
 
109
- def append_header
110
- @headers.add(@field, @field_value)
111
- @reading_header_value = false
112
- @field_value = +""
113
- @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
114
126
  end
115
127
  end
116
128
  end