http 2.2.2 → 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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