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,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
 
4
5
  require "http/form_data"
@@ -23,7 +24,7 @@ module HTTP
23
24
  end
24
25
 
25
26
  # Make an HTTP request
26
- def request(verb, uri, opts = {})
27
+ def request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash
27
28
  opts = @default_options.merge(opts)
28
29
  uri = make_request_uri(uri, opts)
29
30
  headers = make_request_headers(opts)
@@ -31,11 +32,12 @@ module HTTP
31
32
  proxy = opts.proxy
32
33
 
33
34
  req = HTTP::Request.new(
34
- :verb => verb,
35
- :uri => uri,
36
- :headers => headers,
37
- :proxy => proxy,
38
- :body => body
35
+ :verb => verb,
36
+ :uri => uri,
37
+ :headers => headers,
38
+ :proxy => proxy,
39
+ :body => body,
40
+ :auto_deflate => opts.feature(:auto_deflate)
39
41
  )
40
42
 
41
43
  res = perform(req, opts)
@@ -146,29 +148,30 @@ module HTTP
146
148
  headers[Headers::COOKIE] = cookies
147
149
  end
148
150
 
151
+ if (auto_deflate = opts.feature(:auto_deflate))
152
+ # We need to delete Content-Length header. It will be set automatically
153
+ # by HTTP::Request::Writer
154
+ headers.delete(Headers::CONTENT_LENGTH)
155
+
156
+ headers[Headers::CONTENT_ENCODING] = auto_deflate.method
157
+ end
158
+
149
159
  headers
150
160
  end
151
161
 
152
162
  # Create the request body object to send
153
163
  def make_request_body(opts, headers)
154
- request_body =
155
- case
156
- when opts.body
157
- opts.body
158
- when opts.form
159
- form = HTTP::FormData.create opts.form
160
- headers[Headers::CONTENT_TYPE] ||= form.content_type
161
- headers[Headers::CONTENT_LENGTH] ||= form.content_length
162
- form.to_s
163
- when opts.json
164
- body = MimeType[:json].encode opts.json
165
- headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
166
- body
167
- end
168
- if (auto_deflate = opts.feature(:auto_deflate))
169
- auto_deflate.deflate(headers, request_body)
170
- else
171
- request_body
164
+ case
165
+ when opts.body
166
+ opts.body
167
+ when opts.form
168
+ form = HTTP::FormData.create opts.form
169
+ headers[Headers::CONTENT_TYPE] ||= form.content_type
170
+ form
171
+ when opts.json
172
+ body = MimeType[:json].encode opts.json
173
+ headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
174
+ body
172
175
  end
173
176
  end
174
177
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
 
4
5
  require "http/headers"
@@ -10,17 +11,17 @@ module HTTP
10
11
  extend Forwardable
11
12
 
12
13
  # Allowed values for CONNECTION header
13
- KEEP_ALIVE = "Keep-Alive".freeze
14
- CLOSE = "close".freeze
14
+ KEEP_ALIVE = "Keep-Alive"
15
+ CLOSE = "close"
15
16
 
16
17
  # Attempt to read this much data
17
18
  BUFFER_SIZE = 16_384
18
19
 
19
20
  # HTTP/1.0
20
- HTTP_1_0 = "1.0".freeze
21
+ HTTP_1_0 = "1.0"
21
22
 
22
23
  # HTTP/1.1
23
- HTTP_1_1 = "1.1".freeze
24
+ HTTP_1_1 = "1.1"
24
25
 
25
26
  # Returned after HTTP CONNECT (via proxy)
26
27
  attr_reader :proxy_response_headers
@@ -211,6 +212,7 @@ module HTTP
211
212
 
212
213
  value = @socket.readpartial(size)
213
214
  if value == :eof
215
+ @parser << ""
214
216
  :eof
215
217
  elsif value
216
218
  @parser << value
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  ContentType = Struct.new(:mime_type, :charset) do
4
5
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  # Generic error
4
5
  class Error < StandardError; end
@@ -18,6 +19,6 @@ module HTTP
18
19
  # Generic Timeout error
19
20
  class TimeoutError < Error; end
20
21
 
21
- # Header name is invalid
22
- class InvalidHeaderNameError < Error; end
22
+ # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
23
+ class HeaderError < Error; end
23
24
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  class Feature
4
- def initialize(opts = {})
5
+ def initialize(opts = {}) # rubocop:disable Style/OptionHash
5
6
  @opts = opts
6
7
  end
7
8
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
+ require "tempfile"
4
5
 
5
6
  module HTTP
6
7
  module Features
@@ -12,32 +13,88 @@ module HTTP
12
13
 
13
14
  @method = @opts.key?(:method) ? @opts[:method].to_s : "gzip"
14
15
 
15
- raise Error, "Only gzip and deflate methods are supported" unless %w(gzip deflate).include?(@method)
16
+ raise Error, "Only gzip and deflate methods are supported" unless %w[gzip deflate].include?(@method)
16
17
  end
17
18
 
18
- def deflate(headers, body)
19
- return body unless body
20
- return body unless body.is_a?(String)
19
+ def deflated_body(body)
20
+ case method
21
+ when "gzip"
22
+ GzippedBody.new(body)
23
+ when "deflate"
24
+ DeflatedBody.new(body)
25
+ else
26
+ raise ArgumentError, "Unsupported deflate method: #{method}"
27
+ end
28
+ end
21
29
 
22
- # We need to delete Content-Length header. It will be set automatically
23
- # by HTTP::Request::Writer
24
- headers.delete(Headers::CONTENT_LENGTH)
30
+ class CompressedBody
31
+ def initialize(body)
32
+ @body = body
33
+ @compressed = nil
34
+ end
25
35
 
26
- headers[Headers::CONTENT_ENCODING] = method
36
+ def size
37
+ compress_all! unless @compressed
38
+ @compressed.size
39
+ end
27
40
 
28
- case method
29
- when "gzip" then
30
- StringIO.open do |out|
31
- Zlib::GzipWriter.wrap(out) do |gz|
32
- gz.write body
33
- gz.finish
34
- out.tap(&:rewind).read
35
- end
41
+ def each(&block)
42
+ return to_enum __method__ unless block
43
+
44
+ if @compressed
45
+ compressed_each(&block)
46
+ else
47
+ compress(&block)
36
48
  end
37
- when "deflate" then
38
- Zlib::Deflate.deflate(body)
39
- else
40
- raise ArgumentError, "Unsupported deflate method: #{method}"
49
+
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ def compressed_each
56
+ while (data = @compressed.read(Connection::BUFFER_SIZE))
57
+ yield data
58
+ end
59
+ ensure
60
+ @compressed.close!
61
+ end
62
+
63
+ def compress_all!
64
+ @compressed = Tempfile.new("http-compressed_body", :binmode => true)
65
+ compress { |data| @compressed.write(data) }
66
+ @compressed.rewind
67
+ end
68
+ end
69
+
70
+ class GzippedBody < CompressedBody
71
+ def compress(&block)
72
+ gzip = Zlib::GzipWriter.new(BlockIO.new(block))
73
+ @body.each { |chunk| gzip.write(chunk) }
74
+ ensure
75
+ gzip.finish
76
+ end
77
+
78
+ class BlockIO
79
+ def initialize(block)
80
+ @block = block
81
+ end
82
+
83
+ def write(data)
84
+ @block.call(data)
85
+ end
86
+ end
87
+ end
88
+
89
+ class DeflatedBody < CompressedBody
90
+ def compress
91
+ deflater = Zlib::Deflate.new
92
+
93
+ @body.each { |chunk| yield deflater.deflate(chunk) }
94
+
95
+ yield deflater.finish
96
+ ensure
97
+ deflater.close
41
98
  end
42
99
  end
43
100
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  module Features
4
5
  class AutoInflate < Feature
5
6
  def stream_for(connection, response)
6
- if %w(deflate gzip x-gzip).include?(response.headers[:content_encoding])
7
+ if %w[deflate gzip x-gzip].include?(response.headers[:content_encoding])
7
8
  Response::Inflater.new(connection)
8
9
  else
9
10
  connection
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
 
4
5
  require "http/errors"
@@ -196,7 +197,7 @@ module HTTP
196
197
  # Transforms `name` to canonical HTTP header capitalization
197
198
  #
198
199
  # @param [String] name
199
- # @raise [InvalidHeaderNameError] if normalized name does not
200
+ # @raise [HeaderError] if normalized name does not
200
201
  # match {HEADER_NAME_RE}
201
202
  # @return [String] canonical HTTP header name
202
203
  def normalize_header(name)
@@ -206,7 +207,7 @@ module HTTP
206
207
 
207
208
  return normalized if normalized =~ COMPLIANT_NAME_RE
208
209
 
209
- raise InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}"
210
+ raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
210
211
  end
211
212
  end
212
213
  end
@@ -1,83 +1,84 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  class Headers
4
5
  # Content-Types that are acceptable for the response.
5
- ACCEPT = "Accept".freeze
6
+ ACCEPT = "Accept"
6
7
 
7
8
  # The age the object has been in a proxy cache in seconds.
8
- AGE = "Age".freeze
9
+ AGE = "Age"
9
10
 
10
11
  # Authentication credentials for HTTP authentication.
11
- AUTHORIZATION = "Authorization".freeze
12
+ AUTHORIZATION = "Authorization"
12
13
 
13
14
  # Used to specify directives that must be obeyed by all caching mechanisms
14
15
  # along the request-response chain.
15
- CACHE_CONTROL = "Cache-Control".freeze
16
+ CACHE_CONTROL = "Cache-Control"
16
17
 
17
18
  # An HTTP cookie previously sent by the server with Set-Cookie.
18
- COOKIE = "Cookie".freeze
19
+ COOKIE = "Cookie"
19
20
 
20
21
  # Control options for the current connection and list
21
22
  # of hop-by-hop request fields.
22
- CONNECTION = "Connection".freeze
23
+ CONNECTION = "Connection"
23
24
 
24
25
  # The length of the request body in octets (8-bit bytes).
25
- CONTENT_LENGTH = "Content-Length".freeze
26
+ CONTENT_LENGTH = "Content-Length"
26
27
 
27
28
  # The MIME type of the body of the request
28
29
  # (used with POST and PUT requests).
29
- CONTENT_TYPE = "Content-Type".freeze
30
+ CONTENT_TYPE = "Content-Type"
30
31
 
31
32
  # The date and time that the message was sent (in "HTTP-date" format as
32
33
  # defined by RFC 7231 Date/Time Formats).
33
- DATE = "Date".freeze
34
+ DATE = "Date"
34
35
 
35
36
  # An identifier for a specific version of a resource,
36
37
  # often a message digest.
37
- ETAG = "ETag".freeze
38
+ ETAG = "ETag"
38
39
 
39
40
  # Gives the date/time after which the response is considered stale (in
40
41
  # "HTTP-date" format as defined by RFC 7231).
41
- EXPIRES = "Expires".freeze
42
+ EXPIRES = "Expires"
42
43
 
43
44
  # The domain name of the server (for virtual hosting), and the TCP port
44
45
  # number on which the server is listening. The port number may be omitted
45
46
  # if the port is the standard port for the service requested.
46
- HOST = "Host".freeze
47
+ HOST = "Host"
47
48
 
48
49
  # Allows a 304 Not Modified to be returned if content is unchanged.
49
- IF_MODIFIED_SINCE = "If-Modified-Since".freeze
50
+ IF_MODIFIED_SINCE = "If-Modified-Since"
50
51
 
51
52
  # Allows a 304 Not Modified to be returned if content is unchanged.
52
- IF_NONE_MATCH = "If-None-Match".freeze
53
+ IF_NONE_MATCH = "If-None-Match"
53
54
 
54
55
  # The last modified date for the requested object (in "HTTP-date" format as
55
56
  # defined by RFC 7231).
56
- LAST_MODIFIED = "Last-Modified".freeze
57
+ LAST_MODIFIED = "Last-Modified"
57
58
 
58
59
  # Used in redirection, or when a new resource has been created.
59
- LOCATION = "Location".freeze
60
+ LOCATION = "Location"
60
61
 
61
62
  # Authorization credentials for connecting to a proxy.
62
- PROXY_AUTHORIZATION = "Proxy-Authorization".freeze
63
+ PROXY_AUTHORIZATION = "Proxy-Authorization"
63
64
 
64
65
  # An HTTP cookie.
65
- SET_COOKIE = "Set-Cookie".freeze
66
+ SET_COOKIE = "Set-Cookie"
66
67
 
67
68
  # The form of encoding used to safely transfer the entity to the user.
68
69
  # Currently defined methods are: chunked, compress, deflate, gzip, identity.
69
- TRANSFER_ENCODING = "Transfer-Encoding".freeze
70
+ TRANSFER_ENCODING = "Transfer-Encoding"
70
71
 
71
72
  # Indicates what additional content codings have been applied to the
72
73
  # entity-body.
73
- CONTENT_ENCODING = "Content-Encoding".freeze
74
+ CONTENT_ENCODING = "Content-Encoding"
74
75
 
75
76
  # The user agent string of the user agent.
76
- USER_AGENT = "User-Agent".freeze
77
+ USER_AGENT = "User-Agent"
77
78
 
78
79
  # Tells downstream proxies how to match future request headers to decide
79
80
  # whether the cached response can be used rather than requesting a fresh
80
81
  # one from the origin server.
81
- VARY = "Vary".freeze
82
+ VARY = "Vary"
82
83
  end
83
84
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
 
4
5
  module HTTP
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module HTTP
3
4
  # MIME type encode/decode adapters
4
5
  module MimeType
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "forwardable"
3
4
  require "singleton"
4
5
 
@@ -13,7 +14,7 @@ module HTTP
13
14
  def_delegators :instance, :encode, :decode
14
15
  end
15
16
 
16
- %w(encode decode).each do |operation|
17
+ %w[encode decode].each do |operation|
17
18
  class_eval <<-RUBY, __FILE__, __LINE__
18
19
  def #{operation}(*)
19
20
  fail Error, "\#{self.class} does not supports ##{operation}"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "json"
3
4
  require "http/mime_type/adapter"
4
5
 
@@ -14,7 +15,7 @@ module HTTP
14
15
 
15
16
  # Decodes JSON
16
17
  def decode(str)
17
- ::JSON.load str
18
+ ::JSON.parse str
18
19
  end
19
20
  end
20
21