http 2.2.2 → 3.0.0.pre

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +46 -13
  3. data/.travis.yml +17 -12
  4. data/CHANGES.md +25 -1
  5. data/Gemfile +11 -4
  6. data/Guardfile +2 -0
  7. data/README.md +4 -5
  8. data/Rakefile +14 -13
  9. data/http.gemspec +3 -1
  10. data/lib/http.rb +1 -0
  11. data/lib/http/chainable.rb +15 -14
  12. data/lib/http/client.rb +27 -24
  13. data/lib/http/connection.rb +6 -4
  14. data/lib/http/content_type.rb +1 -0
  15. data/lib/http/errors.rb +3 -2
  16. data/lib/http/feature.rb +2 -1
  17. data/lib/http/features/auto_deflate.rb +77 -20
  18. data/lib/http/features/auto_inflate.rb +2 -1
  19. data/lib/http/headers.rb +3 -2
  20. data/lib/http/headers/known.rb +23 -22
  21. data/lib/http/headers/mixin.rb +1 -0
  22. data/lib/http/mime_type.rb +1 -0
  23. data/lib/http/mime_type/adapter.rb +2 -1
  24. data/lib/http/mime_type/json.rb +2 -1
  25. data/lib/http/options.rb +15 -12
  26. data/lib/http/redirector.rb +4 -3
  27. data/lib/http/request.rb +25 -10
  28. data/lib/http/request/body.rb +67 -0
  29. data/lib/http/request/writer.rb +32 -37
  30. data/lib/http/response.rb +17 -2
  31. data/lib/http/response/body.rb +16 -12
  32. data/lib/http/response/parser.rb +1 -0
  33. data/lib/http/response/status.rb +1 -0
  34. data/lib/http/response/status/reasons.rb +1 -0
  35. data/lib/http/timeout/global.rb +1 -0
  36. data/lib/http/timeout/null.rb +2 -1
  37. data/lib/http/timeout/per_operation.rb +19 -6
  38. data/lib/http/uri.rb +8 -2
  39. data/lib/http/version.rb +1 -1
  40. data/spec/lib/http/client_spec.rb +104 -4
  41. data/spec/lib/http/content_type_spec.rb +1 -0
  42. data/spec/lib/http/features/auto_deflate_spec.rb +32 -64
  43. data/spec/lib/http/features/auto_inflate_spec.rb +1 -0
  44. data/spec/lib/http/headers/mixin_spec.rb +1 -0
  45. data/spec/lib/http/headers_spec.rb +36 -35
  46. data/spec/lib/http/options/body_spec.rb +1 -0
  47. data/spec/lib/http/options/features_spec.rb +1 -0
  48. data/spec/lib/http/options/form_spec.rb +1 -0
  49. data/spec/lib/http/options/headers_spec.rb +2 -1
  50. data/spec/lib/http/options/json_spec.rb +1 -0
  51. data/spec/lib/http/options/new_spec.rb +2 -1
  52. data/spec/lib/http/options/proxy_spec.rb +1 -0
  53. data/spec/lib/http/options_spec.rb +1 -0
  54. data/spec/lib/http/redirector_spec.rb +1 -0
  55. data/spec/lib/http/request/body_spec.rb +138 -0
  56. data/spec/lib/http/request/writer_spec.rb +44 -74
  57. data/spec/lib/http/request_spec.rb +14 -0
  58. data/spec/lib/http/response/body_spec.rb +20 -4
  59. data/spec/lib/http/response/status_spec.rb +27 -26
  60. data/spec/lib/http/response_spec.rb +10 -0
  61. data/spec/lib/http/uri_spec.rb +11 -0
  62. data/spec/lib/http_spec.rb +18 -6
  63. data/spec/regression_specs.rb +1 -0
  64. data/spec/spec_helper.rb +1 -0
  65. data/spec/support/black_hole.rb +9 -2
  66. data/spec/support/capture_warning.rb +1 -0
  67. data/spec/support/dummy_server.rb +2 -1
  68. data/spec/support/dummy_server/servlet.rb +1 -1
  69. data/spec/support/fakeio.rb +21 -0
  70. data/spec/support/http_handling_shared.rb +1 -0
  71. data/spec/support/proxy_server.rb +1 -0
  72. data/spec/support/servers/config.rb +1 -0
  73. data/spec/support/servers/runner.rb +1 -0
  74. data/spec/support/ssl_helper.rb +3 -2
  75. metadata +20 -9
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ClassLength, Style/RedundantSelf
4
+
2
5
  require "http/headers"
3
6
  require "openssl"
4
7
  require "socket"
@@ -8,7 +11,6 @@ require "http/features/auto_inflate"
8
11
  require "http/features/auto_deflate"
9
12
 
10
13
  module HTTP
11
- # rubocop:disable Metrics/ClassLength
12
14
  class Options
13
15
  @default_socket_class = TCPSocket
14
16
  @default_ssl_socket_class = OpenSSL::SSL::SSLSocket
@@ -22,7 +24,7 @@ module HTTP
22
24
  attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
23
25
  attr_reader :available_features
24
26
 
25
- def new(options = {})
27
+ def new(options = {}) # rubocop:disable Style/OptionHash
26
28
  return options if options.is_a?(self)
27
29
  super
28
30
  end
@@ -46,7 +48,7 @@ module HTTP
46
48
  end
47
49
  end
48
50
 
49
- def initialize(options = {})
51
+ def initialize(options = {}) # rubocop:disable Style/OptionHash
50
52
  defaults = {
51
53
  :response => :auto,
52
54
  :proxy => {},
@@ -115,21 +117,22 @@ module HTTP
115
117
  end
116
118
  end
117
119
 
118
- %w(
120
+ %w[
119
121
  proxy params form json body follow response
120
122
  socket_class nodelay ssl_socket_class ssl_context ssl
121
123
  persistent keep_alive_timeout timeout_class timeout_options
122
- ).each do |method_name|
124
+ ].each do |method_name|
123
125
  def_option method_name
124
126
  end
125
127
 
126
128
  def follow=(value)
127
- @follow = case
128
- when !value then nil
129
- when true == value then {}
130
- when value.respond_to?(:fetch) then value
131
- else argument_error! "Unsupported follow options: #{value}"
132
- end
129
+ @follow =
130
+ case
131
+ when !value then nil
132
+ when true == value then {}
133
+ when value.respond_to?(:fetch) then value
134
+ else argument_error! "Unsupported follow options: #{value}"
135
+ end
133
136
  end
134
137
 
135
138
  def persistent=(value)
@@ -182,7 +185,7 @@ module HTTP
182
185
  private
183
186
 
184
187
  def argument_error!(message)
185
- raise(Error, message, caller[1..-1])
188
+ raise(Error, message, caller(1..-1))
186
189
  end
187
190
  end
188
191
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "set"
3
4
 
4
5
  require "http/headers"
@@ -20,10 +21,10 @@ module HTTP
20
21
 
21
22
  # Insecure http verbs, which should trigger StateError in strict mode
22
23
  # upon {STRICT_SENSITIVE_CODES}
23
- UNSAFE_VERBS = [:put, :delete, :post].to_set.freeze
24
+ UNSAFE_VERBS = %i[put delete post].to_set.freeze
24
25
 
25
26
  # Verbs which will remain unchanged upon See Other response.
26
- SEE_OTHER_ALLOWED_VERBS = [:get, :head].to_set.freeze
27
+ SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
27
28
 
28
29
  # @!attribute [r] strict
29
30
  # Returns redirector policy.
@@ -38,7 +39,7 @@ module HTTP
38
39
  # @param [Hash] opts
39
40
  # @option opts [Boolean] :strict (true) redirector hops policy
40
41
  # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
41
- def initialize(opts = {})
42
+ def initialize(opts = {}) # rubocop:disable Style/OptionHash
42
43
  @strict = opts.fetch(:strict, true)
43
44
  @max_hops = opts.fetch(:max_hops, 5).to_i
44
45
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
  require "base64"
4
5
  require "time"
5
6
 
6
7
  require "http/errors"
7
8
  require "http/headers"
9
+ require "http/request/body"
8
10
  require "http/request/writer"
9
11
  require "http/version"
10
12
  require "http/uri"
@@ -22,7 +24,7 @@ module HTTP
22
24
  class UnsupportedSchemeError < RequestError; end
23
25
 
24
26
  # Default User-Agent header value
25
- USER_AGENT = "http.rb/#{HTTP::VERSION}".freeze
27
+ USER_AGENT = "http.rb/#{HTTP::VERSION}"
26
28
 
27
29
  METHODS = [
28
30
  # RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
@@ -48,7 +50,7 @@ module HTTP
48
50
  ].freeze
49
51
 
50
52
  # Allowed schemes
51
- SCHEMES = [:http, :https, :ws, :wss].freeze
53
+ SCHEMES = %i[http https ws wss].freeze
52
54
 
53
55
  # Default ports of supported schemes
54
56
  PORTS = {
@@ -74,7 +76,7 @@ module HTTP
74
76
  # @option opts [HTTP::URI, #to_s] :uri
75
77
  # @option opts [Hash] :headers
76
78
  # @option opts [Hash] :proxy
77
- # @option opts [String] :body
79
+ # @option opts [String, Enumerable, IO, nil] :body
78
80
  def initialize(opts)
79
81
  @verb = opts.fetch(:verb).to_s.downcase.to_sym
80
82
  @uri = normalize_uri(opts.fetch(:uri))
@@ -84,7 +86,7 @@ module HTTP
84
86
  raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
85
87
 
86
88
  @proxy = opts[:proxy] || {}
87
- @body = opts[:body]
89
+ @body = request_body(opts[:body], opts)
88
90
  @version = opts[:version] || "1.1"
89
91
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
90
92
 
@@ -94,7 +96,10 @@ module HTTP
94
96
 
95
97
  # Returns new Request with updated uri
96
98
  def redirect(uri, verb = @verb)
97
- req = self.class.new(
99
+ headers = self.headers.dup
100
+ headers.delete(Headers::HOST)
101
+
102
+ self.class.new(
98
103
  :verb => verb,
99
104
  :uri => @uri.join(uri),
100
105
  :headers => headers,
@@ -102,9 +107,6 @@ module HTTP
102
107
  :body => body,
103
108
  :version => version
104
109
  )
105
-
106
- req[Headers::HOST] = req.uri.host
107
- req
108
110
  end
109
111
 
110
112
  # Stream the request to a socket
@@ -145,8 +147,14 @@ module HTTP
145
147
 
146
148
  # Compute HTTP request header for direct or proxy request
147
149
  def headline
148
- request_uri = (using_proxy? && !uri.https?) ? uri : uri.omit(:scheme, :authority)
149
- "#{verb.to_s.upcase} #{request_uri.omit :fragment} HTTP/#{version}"
150
+ request_uri =
151
+ if using_proxy? && !uri.https?
152
+ uri.omit(:fragment)
153
+ else
154
+ uri.omit(:scheme, :authority, :fragment)
155
+ end
156
+
157
+ "#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
150
158
  end
151
159
 
152
160
  # Compute HTTP request header SSL proxy connection
@@ -178,6 +186,13 @@ module HTTP
178
186
 
179
187
  private
180
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
194
+ end
195
+
181
196
  # @!attribute [r] host
182
197
  # @return [String]
183
198
  def_delegator :@uri, :host
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Request
5
+ class Body
6
+ def initialize(body)
7
+ @body = body
8
+
9
+ validate_body_type!
10
+ end
11
+
12
+ # Returns size which should be used for the "Content-Length" header.
13
+ #
14
+ # @return [Integer]
15
+ def size
16
+ if @body.is_a?(String)
17
+ @body.bytesize
18
+ elsif @body.respond_to?(:read)
19
+ raise RequestError, "IO object must respond to #size" unless @body.respond_to?(:size)
20
+ @body.size
21
+ elsif @body.nil?
22
+ 0
23
+ else
24
+ raise RequestError, "cannot determine size of body: #{@body.inspect}"
25
+ end
26
+ end
27
+
28
+ # Yields chunks of content to be streamed to the request body.
29
+ #
30
+ # @yieldparam [String]
31
+ def each(&block)
32
+ if @body.is_a?(String)
33
+ yield @body
34
+ elsif @body.respond_to?(:read)
35
+ IO.copy_stream(@body, ProcIO.new(block))
36
+ elsif @body.is_a?(Enumerable)
37
+ @body.each(&block)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def validate_body_type!
44
+ return if @body.is_a?(String)
45
+ return if @body.respond_to?(:read)
46
+ return if @body.is_a?(Enumerable)
47
+ return if @body.nil?
48
+
49
+ raise RequestError, "body of wrong type: #{@body.class}"
50
+ end
51
+
52
+ # This class provides a "writable IO" wrapper around a proc object, with
53
+ # #write simply calling the proc, which we can pass in as the
54
+ # "destination IO" in IO.copy_stream.
55
+ class ProcIO
56
+ def initialize(block)
57
+ @block = block
58
+ end
59
+
60
+ def write(data)
61
+ @block.call(data)
62
+ data.bytesize
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,31 +1,27 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "http/headers"
3
4
 
4
5
  module HTTP
5
6
  class Request
6
7
  class Writer
7
8
  # CRLF is the universal HTTP delimiter
8
- CRLF = "\r\n".freeze
9
+ CRLF = "\r\n"
9
10
 
10
11
  # Chunked data termintaor.
11
- ZERO = "0".freeze
12
+ ZERO = "0"
12
13
 
13
14
  # Chunked transfer encoding
14
- CHUNKED = "chunked".freeze
15
+ CHUNKED = "chunked"
15
16
 
16
17
  # End of a chunked transfer
17
- CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}".freeze
18
-
19
- # Types valid to be used as body source
20
- VALID_BODY_TYPES = [String, NilClass, Enumerable].freeze
18
+ CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}"
21
19
 
22
20
  def initialize(socket, body, headers, headline)
23
21
  @body = body
24
22
  @socket = socket
25
23
  @headers = headers
26
24
  @request_header = [headline]
27
-
28
- validate_body_type!
29
25
  end
30
26
 
31
27
  # Adds headers to the request header from the headers array
@@ -51,13 +47,9 @@ module HTTP
51
47
  # Adds the headers to the header array for the given request body we are working
52
48
  # with
53
49
  def add_body_type_headers
54
- if @body.is_a?(String) && !@headers[Headers::CONTENT_LENGTH]
55
- @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.bytesize}"
56
- elsif @body.nil? && !@headers[Headers::CONTENT_LENGTH]
57
- @request_header << "#{Headers::CONTENT_LENGTH}: 0"
58
- elsif @body.is_a?(Enumerable) && CHUNKED != @headers[Headers::TRANSFER_ENCODING]
59
- raise(RequestError, "invalid transfer encoding")
60
- end
50
+ return if @headers[Headers::CONTENT_LENGTH] || chunked?
51
+
52
+ @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
61
53
  end
62
54
 
63
55
  # Joins the headers specified in the request into a correctly formatted
@@ -69,29 +61,37 @@ module HTTP
69
61
  end
70
62
 
71
63
  def send_request
72
- headers = join_headers
73
-
74
64
  # It's important to send the request in a single write call when
75
65
  # possible in order to play nicely with Nagle's algorithm. Making
76
66
  # two writes in a row triggers a pathological case where Nagle is
77
67
  # expecting a third write that never happens.
78
- case @body
79
- when NilClass
80
- write(headers)
81
- when String
82
- write(headers << @body)
83
- when Enumerable
84
- write(headers)
85
-
86
- @body.each do |chunk|
87
- write(chunk.bytesize.to_s(16) << CRLF << chunk << CRLF)
88
- end
89
-
90
- write(CHUNKED_END)
91
- else raise TypeError, "invalid body type: #{@body.class}"
68
+ data = join_headers
69
+
70
+ @body.each do |chunk|
71
+ data << encode_chunk(chunk)
72
+ write(data)
73
+ data.clear
74
+ end
75
+
76
+ write(data) unless data.empty?
77
+
78
+ write(CHUNKED_END) if chunked?
79
+ end
80
+
81
+ # Returns the chunk encoded for to the specified "Transfer-Encoding" header.
82
+ def encode_chunk(chunk)
83
+ if chunked?
84
+ chunk.bytesize.to_s(16) << CRLF << chunk << CRLF
85
+ else
86
+ chunk
92
87
  end
93
88
  end
94
89
 
90
+ # Returns true if the request should be sent in chunked encoding.
91
+ def chunked?
92
+ @headers[Headers::TRANSFER_ENCODING] == CHUNKED
93
+ end
94
+
95
95
  private
96
96
 
97
97
  def write(data)
@@ -101,11 +101,6 @@ module HTTP
101
101
  data = data.byteslice(length..-1)
102
102
  end
103
103
  end
104
-
105
- def validate_body_type!
106
- return if VALID_BODY_TYPES.any? { |type| @body.is_a? type }
107
- raise RequestError, "body of wrong type: #{@body.class}"
108
- end
109
104
  end
110
105
  end
111
106
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
 
4
5
  require "http/headers"
@@ -50,7 +51,7 @@ module HTTP
50
51
  encoding = opts[:encoding] || charset || Encoding::BINARY
51
52
  stream = body_stream_for(connection, opts)
52
53
 
53
- @body = Response::Body.new(stream, encoding)
54
+ @body = Response::Body.new(stream, :encoding => encoding)
54
55
  else
55
56
  @body = opts.fetch(:body)
56
57
  end
@@ -98,8 +99,13 @@ module HTTP
98
99
  # (not an integer, e.g. empty string or string with non-digits).
99
100
  # @return [Integer] otherwise
100
101
  def content_length
102
+ # http://greenbytes.de/tech/webdav/rfc7230.html#rfc.section.3.3.3
103
+ # Clause 3: "If a message is received with both a Transfer-Encoding
104
+ # and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.
105
+ return nil if @headers.include?(Headers::TRANSFER_ENCODING)
106
+
101
107
  value = @headers[Headers::CONTENT_LENGTH]
102
- return unless value
108
+ return nil unless value
103
109
 
104
110
  begin
105
111
  Integer(value)
@@ -131,6 +137,15 @@ module HTTP
131
137
  end
132
138
  end
133
139
 
140
+ def chunked?
141
+ return false unless @headers.include?(Headers::TRANSFER_ENCODING)
142
+
143
+ encoding = @headers.get(Headers::TRANSFER_ENCODING)
144
+
145
+ # TODO: "chunked" is frozen in the request writer. How about making it accessible?
146
+ encoding.last == "chunked"
147
+ end
148
+
134
149
  # Parse response body with corresponding MIME type adapter.
135
150
  #
136
151
  # @param [#to_s] as Parse as given MIME type
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
  require "http/client"
4
5
 
@@ -15,18 +16,19 @@ module HTTP
15
16
  # @return [HTTP::Connection]
16
17
  attr_reader :connection
17
18
 
18
- def initialize(stream, encoding = Encoding::BINARY)
19
+ def initialize(stream, encoding: Encoding::BINARY)
19
20
  @stream = stream
20
21
  @connection = stream.is_a?(Inflater) ? stream.connection : stream
21
22
  @streaming = nil
22
23
  @contents = nil
23
- @encoding = encoding
24
+ @encoding = find_encoding(encoding)
24
25
  end
25
26
 
26
27
  # (see HTTP::Client#readpartial)
27
28
  def readpartial(*args)
28
29
  stream!
29
- @stream.readpartial(*args)
30
+ chunk = @stream.readpartial(*args)
31
+ chunk.force_encoding(@encoding) if chunk
30
32
  end
31
33
 
32
34
  # Iterate over the body, allowing it to be enumerable
@@ -42,19 +44,12 @@ module HTTP
42
44
 
43
45
  raise StateError, "body is being streamed" unless @streaming.nil?
44
46
 
45
- # see issue 312
46
- begin
47
- encoding = Encoding.find @encoding
48
- rescue ArgumentError
49
- encoding = Encoding::BINARY
50
- end
51
-
52
47
  begin
53
48
  @streaming = false
54
- @contents = String.new("").force_encoding(encoding)
49
+ @contents = String.new("").force_encoding(@encoding)
55
50
 
56
51
  while (chunk = @stream.readpartial)
57
- @contents << chunk.force_encoding(encoding)
52
+ @contents << chunk.force_encoding(@encoding)
58
53
  end
59
54
  rescue
60
55
  @contents = nil
@@ -75,6 +70,15 @@ module HTTP
75
70
  def inspect
76
71
  "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
77
72
  end
73
+
74
+ private
75
+
76
+ # Retrieve encoding by name. If encoding cannot be found, default to binary.
77
+ def find_encoding(encoding)
78
+ Encoding.find encoding
79
+ rescue ArgumentError
80
+ Encoding::BINARY
81
+ end
78
82
  end
79
83
  end
80
84
  end