http 4.4.1 → 5.0.1

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.
Files changed (68) 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 +8 -110
  6. data/.rubocop/layout.yml +8 -0
  7. data/.rubocop/style.yml +32 -0
  8. data/.rubocop_todo.yml +192 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +112 -3
  11. data/Gemfile +18 -10
  12. data/LICENSE.txt +1 -1
  13. data/README.md +17 -20
  14. data/Rakefile +2 -10
  15. data/http.gemspec +3 -3
  16. data/lib/http/chainable.rb +23 -17
  17. data/lib/http/client.rb +36 -30
  18. data/lib/http/connection.rb +11 -7
  19. data/lib/http/content_type.rb +12 -7
  20. data/lib/http/feature.rb +3 -1
  21. data/lib/http/features/auto_deflate.rb +6 -6
  22. data/lib/http/features/auto_inflate.rb +6 -5
  23. data/lib/http/features/instrumentation.rb +1 -1
  24. data/lib/http/features/logging.rb +19 -21
  25. data/lib/http/headers.rb +50 -13
  26. data/lib/http/mime_type/adapter.rb +3 -1
  27. data/lib/http/mime_type/json.rb +1 -0
  28. data/lib/http/options.rb +5 -8
  29. data/lib/http/redirector.rb +2 -1
  30. data/lib/http/request.rb +28 -11
  31. data/lib/http/request/body.rb +1 -0
  32. data/lib/http/request/writer.rb +3 -2
  33. data/lib/http/response.rb +17 -15
  34. data/lib/http/response/body.rb +2 -2
  35. data/lib/http/response/inflater.rb +1 -1
  36. data/lib/http/response/parser.rb +74 -62
  37. data/lib/http/response/status.rb +4 -3
  38. data/lib/http/timeout/global.rb +17 -31
  39. data/lib/http/timeout/null.rb +2 -1
  40. data/lib/http/timeout/per_operation.rb +31 -54
  41. data/lib/http/uri.rb +5 -5
  42. data/lib/http/version.rb +1 -1
  43. data/spec/lib/http/client_spec.rb +119 -30
  44. data/spec/lib/http/connection_spec.rb +8 -5
  45. data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
  46. data/spec/lib/http/features/instrumentation_spec.rb +28 -21
  47. data/spec/lib/http/features/logging_spec.rb +8 -9
  48. data/spec/lib/http/headers_spec.rb +53 -18
  49. data/spec/lib/http/options/headers_spec.rb +1 -1
  50. data/spec/lib/http/options/merge_spec.rb +16 -16
  51. data/spec/lib/http/redirector_spec.rb +46 -1
  52. data/spec/lib/http/request/writer_spec.rb +13 -1
  53. data/spec/lib/http/request_spec.rb +5 -5
  54. data/spec/lib/http/response/parser_spec.rb +33 -4
  55. data/spec/lib/http/response/status_spec.rb +3 -3
  56. data/spec/lib/http/response_spec.rb +5 -3
  57. data/spec/lib/http_spec.rb +30 -3
  58. data/spec/spec_helper.rb +21 -21
  59. data/spec/support/black_hole.rb +1 -1
  60. data/spec/support/dummy_server.rb +7 -7
  61. data/spec/support/dummy_server/servlet.rb +17 -6
  62. data/spec/support/fuubar.rb +21 -0
  63. data/spec/support/http_handling_shared.rb +4 -4
  64. data/spec/support/simplecov.rb +19 -0
  65. data/spec/support/ssl_helper.rb +4 -4
  66. metadata +18 -12
  67. data/.coveralls.yml +0 -1
  68. data/.travis.yml +0 -39
@@ -14,13 +14,15 @@ 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
- class_eval <<-RUBY, __FILE__, __LINE__
19
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
20
  def #{operation}(*)
20
21
  fail Error, "\#{self.class} does not supports ##{operation}"
21
22
  end
22
23
  RUBY
23
24
  end
25
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
24
26
  end
25
27
  end
26
28
  end
@@ -10,6 +10,7 @@ module HTTP
10
10
  # Encodes object to JSON
11
11
  def encode(obj)
12
12
  return obj.to_json if obj.respond_to?(:to_json)
13
+
13
14
  ::JSON.dump obj
14
15
  end
15
16
 
data/lib/http/options.rb CHANGED
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ClassLength
4
-
5
3
  require "http/headers"
6
4
  require "openssl"
7
5
  require "socket"
8
6
  require "http/uri"
9
7
 
10
8
  module HTTP
11
- class Options
9
+ class Options # rubocop:disable Metrics/ClassLength
12
10
  @default_socket_class = TCPSocket
13
11
  @default_ssl_socket_class = OpenSSL::SSL::SSLSocket
14
12
  @default_timeout_class = HTTP::Timeout::Null
@@ -18,9 +16,8 @@ module HTTP
18
16
  attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
19
17
  attr_reader :available_features
20
18
 
21
- def new(options = {}) # rubocop:disable Style/OptionHash
22
- return options if options.is_a?(self)
23
- super
19
+ def new(options = {})
20
+ options.is_a?(self) ? options : super
24
21
  end
25
22
 
26
23
  def defined_options
@@ -35,7 +32,7 @@ module HTTP
35
32
 
36
33
  def def_option(name, reader_only: false, &interpreter)
37
34
  defined_options << name.to_sym
38
- interpreter ||= lambda { |v| v }
35
+ interpreter ||= ->(v) { v }
39
36
 
40
37
  if reader_only
41
38
  attr_reader name
@@ -50,7 +47,7 @@ module HTTP
50
47
  end
51
48
  end
52
49
 
53
- def initialize(options = {}) # rubocop:disable Style/OptionHash
50
+ def initialize(options = {})
54
51
  defaults = {
55
52
  :response => :auto,
56
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
@@ -90,6 +90,7 @@ module HTTP
90
90
 
91
91
  if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
92
92
  raise StateError, "can't follow #{@response.status} redirect" if @strict
93
+
93
94
  verb = :get
94
95
  end
95
96
 
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
@@ -54,10 +57,10 @@ module HTTP
54
57
 
55
58
  # Default ports of supported schemes
56
59
  PORTS = {
57
- :http => 80,
58
- :https => 443,
59
- :ws => 80,
60
- :wss => 443
60
+ :http => 80,
61
+ :https => 443,
62
+ :ws => 80,
63
+ :wss => 443
61
64
  }.freeze
62
65
 
63
66
  # Method is given as a lowercase symbol e.g. :get, :post
@@ -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
  )
@@ -168,8 +185,8 @@ module HTTP
168
185
  # Headers to send with proxy connect request
169
186
  def proxy_connect_headers
170
187
  connect_headers = HTTP::Headers.coerce(
171
- Headers::HOST => headers[Headers::HOST],
172
- Headers::USER_AGENT => headers[Headers::USER_AGENT]
188
+ Headers::HOST => headers[Headers::HOST],
189
+ Headers::USER_AGENT => headers[Headers::USER_AGENT]
173
190
  )
174
191
 
175
192
  connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
@@ -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
@@ -19,6 +19,7 @@ module HTTP
19
19
  @source.bytesize
20
20
  elsif @source.respond_to?(:read)
21
21
  raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
22
+
22
23
  @source.size
23
24
  elsif @source.nil?
24
25
  0
@@ -108,12 +108,13 @@ module HTTP
108
108
  until data.empty?
109
109
  length = @socket.write(data)
110
110
  break unless data.bytesize > length
111
+
111
112
  data = data.byteslice(length..-1)
112
113
  end
113
114
  rescue Errno::EPIPE
114
115
  raise
115
- rescue IOError, SocketError, SystemCallError => ex
116
- raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace
116
+ rescue IOError, SocketError, SystemCallError => e
117
+ raise ConnectionError, "error writing to socket: #{e}", e.backtrace
117
118
  end
118
119
  end
119
120
  end
data/lib/http/response.rb CHANGED
@@ -7,7 +7,6 @@ require "http/content_type"
7
7
  require "http/mime_type"
8
8
  require "http/response/status"
9
9
  require "http/response/inflater"
10
- require "http/uri"
11
10
  require "http/cookie_jar"
12
11
  require "time"
13
12
 
@@ -26,8 +25,8 @@ module HTTP
26
25
  # @return [Body]
27
26
  attr_reader :body
28
27
 
29
- # @return [URI, nil]
30
- attr_reader :uri
28
+ # @return [Request]
29
+ attr_reader :request
31
30
 
32
31
  # @return [Hash]
33
32
  attr_reader :proxy_headers
@@ -41,10 +40,10 @@ module HTTP
41
40
  # @option opts [HTTP::Connection] :connection
42
41
  # @option opts [String] :encoding Encoding to use when reading body
43
42
  # @option opts [String] :body
44
- # @option opts [String] :uri
43
+ # @option opts [HTTP::Request] request
45
44
  def initialize(opts)
46
45
  @version = opts.fetch(:version)
47
- @uri = HTTP::URI.parse(opts.fetch(:uri)) if opts.include? :uri
46
+ @request = opts.fetch(:request)
48
47
  @status = HTTP::Response::Status.new(opts.fetch(:status))
49
48
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
50
49
  @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
@@ -61,24 +60,28 @@ module HTTP
61
60
 
62
61
  # @!method reason
63
62
  # @return (see HTTP::Response::Status#reason)
64
- def_delegator :status, :reason
63
+ def_delegator :@status, :reason
65
64
 
66
65
  # @!method code
67
66
  # @return (see HTTP::Response::Status#code)
68
- def_delegator :status, :code
67
+ def_delegator :@status, :code
69
68
 
70
69
  # @!method to_s
71
70
  # (see HTTP::Response::Body#to_s)
72
- def_delegator :body, :to_s
71
+ def_delegator :@body, :to_s
73
72
  alias to_str to_s
74
73
 
75
74
  # @!method readpartial
76
75
  # (see HTTP::Response::Body#readpartial)
77
- def_delegator :body, :readpartial
76
+ def_delegator :@body, :readpartial
78
77
 
79
78
  # @!method connection
80
79
  # (see HTTP::Response::Body#connection)
81
- def_delegator :body, :connection
80
+ def_delegator :@body, :connection
81
+
82
+ # @!method uri
83
+ # @return (see HTTP::Request#uri)
84
+ def_delegator :@request, :uri
82
85
 
83
86
  # Returns an Array ala Rack: `[status, headers, body]`
84
87
  #
@@ -150,12 +153,11 @@ module HTTP
150
153
 
151
154
  # Parse response body with corresponding MIME type adapter.
152
155
  #
153
- # @param [#to_s] as Parse as given MIME type
154
- # instead of the one determined from headers
155
- # @raise [HTTP::Error] if adapter not found
156
+ # @param type [#to_s] Parse as given MIME type.
157
+ # @raise (see MimeType.[])
156
158
  # @return [Object]
157
- def parse(as = nil)
158
- MimeType[as || mime_type].decode to_s
159
+ def parse(type = nil)
160
+ MimeType[type || mime_type].decode to_s
159
161
  end
160
162
 
161
163
  # Inspect a response
@@ -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
@@ -64,6 +63,7 @@ module HTTP
64
63
  # Assert that the body is actively being streamed
65
64
  def stream!
66
65
  raise StateError, "body has already been consumed" if @streaming == false
66
+
67
67
  @streaming = true
68
68
  end
69
69
 
@@ -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,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