http 2.1.0 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ce75d295d182290642aa569b754914f89ed797f
4
- data.tar.gz: 353078321cc4c0e9134a5b10d655186474288770
3
+ metadata.gz: 516d371c9aa3a19abeaabeefea99bd16be506f36
4
+ data.tar.gz: 030d18563e33fc3aad5832cb98508013cba1a956
5
5
  SHA512:
6
- metadata.gz: d4717d7e9b0a3d841fa31e533e69e35728943218c68455550ccacb845712c1a24e657385c7c4d228c6d3af5a5c68105a674647bef64fba1af33439041153db6a
7
- data.tar.gz: 6dd93161b0a552b4f7484ee2e935e54f57de0f0f6f3f1415250f35073ea90be854baa00322cc59a078a2f3f0a84732289de5ad44e63f782874df44a347770bb8
6
+ metadata.gz: 33f746cb66e0493bb3ca2a272bbf7364a6a949e804dc7f581738a05b373367b08baf0798e838e3a7865d7ef782f0e74dd823204a63bb97b7f334964673e9ed0f
7
+ data.tar.gz: c426e9ff531bbc8268b7bfd3d49276c0fb890e063a69fc3fd6000c8580631dfb62fd83d04088c1a77ed246078fe77c30364c42cdaa527a1f2cedcb1e55499c77
@@ -20,7 +20,7 @@ Metrics/LineLength:
20
20
 
21
21
  Metrics/MethodLength:
22
22
  CountComments: false
23
- Max: 22 # TODO: Lower to 15
23
+ Max: 25 # TODO: Lower to 15
24
24
 
25
25
  Metrics/ModuleLength:
26
26
  CountComments: false
@@ -0,0 +1 @@
1
+ 2.4.0
@@ -1,28 +1,29 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
 
4
- bundler_args: --without development doc
4
+ before_install:
5
+ - gem update --system 2.6.10
6
+ - gem --version
7
+ - gem install bundler --version 1.14.3 --no-rdoc --no-ri
8
+ - bundle --version
9
+
10
+ install: bundle _1.14.3_ install --without development doc
11
+
12
+ script: bundle _1.14.3_ exec rake
5
13
 
6
14
  env:
7
15
  global:
8
16
  - JRUBY_OPTS="$JRUBY_OPTS --debug"
9
17
 
10
18
  rvm:
19
+ - jruby-9.1.7.0
11
20
  - 2.0.0
12
21
  - 2.1
13
22
  - 2.2
14
- - 2.3.0
15
- - 2.3.1
16
- - jruby-9.1.0.0
17
- - jruby-head
18
- - ruby-head
19
- - rbx-2
23
+ - 2.3.3
24
+ - 2.4.0
20
25
 
21
26
  matrix:
22
- allow_failures:
23
- - rvm: jruby-head
24
- - rvm: ruby-head
25
- - rvm: rbx-2
26
27
  fast_finish: true
27
28
 
28
29
  branches:
data/CHANGES.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 2.2.0 (2017-02-03)
2
+
3
+ * [#375](https://github.com/httprb/http/pull/375)
4
+ Add support for automatic Gzip/Inflate
5
+ ([@Bonias])
6
+
7
+ * [#390](https://github.com/httprb/http/pull/390)
8
+ Add REPORT to the list of valid HTTP verbs
9
+ ([@ixti])
10
+
11
+
1
12
  ## 2.1.0 (2016-11-08)
2
13
 
3
14
  * [#370](https://github.com/httprb/http/issues/370)
@@ -566,3 +577,4 @@ end
566
577
  [@jhbabon]: https://github.com/jhbabon
567
578
  [@britishtea]: https://github.com/britishtea
568
579
  [@janko-m]: https://github.com/janko-m
580
+ [@Bonias]: https://github.com/Bonias
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source "https://rubygems.org"
2
+ ruby RUBY_VERSION
2
3
 
3
4
  gem "rake"
4
5
 
@@ -17,13 +18,12 @@ group :test do
17
18
  gem "backports"
18
19
  gem "coveralls", :require => false
19
20
  gem "simplecov", ">= 0.9"
20
- gem "json", ">= 1.8.1"
21
21
  gem "rubocop", "= 0.40.0"
22
22
  gem "rspec", "~> 3.0"
23
23
  gem "rspec-its"
24
24
  gem "yardstick"
25
25
  gem "certificate_authority", :require => false
26
- gem "activemodel", "~> 4", :require => false # Used by certificate_authority
26
+ gem "activemodel", :require => false # Used by certificate_authority
27
27
  end
28
28
 
29
29
  group :doc do
data/README.md CHANGED
@@ -160,7 +160,7 @@ versions:
160
160
  * Ruby 2.1.x
161
161
  * Ruby 2.2.x
162
162
  * Ruby 2.3.x
163
- * JRuby 9.1.0.0
163
+ * JRuby 9.1.x.x
164
164
 
165
165
  If something doesn't work on one of these versions, it's a bug.
166
166
 
@@ -75,10 +75,12 @@ module HTTP
75
75
  branch(options).request verb, uri
76
76
  end
77
77
 
78
- # @overload(options = {})
78
+ # @overload timeout(options = {})
79
79
  # Syntax sugar for `timeout(:per_operation, options)`
80
- # @overload(klass, options = {})
80
+ # @overload timeout(klass, options = {})
81
+ # Adds a timeout to the request.
81
82
  # @param [#to_sym] klass
83
+ # either :null, :global, or :per_operation
82
84
  # @param [Hash] options
83
85
  # @option options [Float] :read Read timeout
84
86
  # @option options [Float] :write Write timeout
@@ -228,6 +230,14 @@ module HTTP
228
230
  branch default_options.with_nodelay(true)
229
231
  end
230
232
 
233
+ # Turn on given features. Available features are:
234
+ # * auto_inflate
235
+ # * auto_deflate
236
+ # @param features
237
+ def use(*features)
238
+ branch default_options.with_features(features)
239
+ end
240
+
231
241
  private
232
242
 
233
243
  # :nodoc:
@@ -71,6 +71,7 @@ module HTTP
71
71
  :proxy_headers => @connection.proxy_response_headers,
72
72
  :connection => @connection,
73
73
  :encoding => options.encoding,
74
+ :auto_inflate => options.feature(:auto_inflate),
74
75
  :uri => req.uri
75
76
  )
76
77
 
@@ -150,18 +151,24 @@ module HTTP
150
151
 
151
152
  # Create the request body object to send
152
153
  def make_request_body(opts, headers)
153
- case
154
- when opts.body
155
- opts.body
156
- when opts.form
157
- form = HTTP::FormData.create opts.form
158
- headers[Headers::CONTENT_TYPE] ||= form.content_type
159
- headers[Headers::CONTENT_LENGTH] ||= form.content_length
160
- form.to_s
161
- when opts.json
162
- body = MimeType[:json].encode opts.json
163
- headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
164
- body
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
165
172
  end
166
173
  end
167
174
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module HTTP
3
+ class Feature
4
+ def initialize(opts = {})
5
+ @opts = opts
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module HTTP
6
+ module Features
7
+ class AutoDeflate < Feature
8
+ attr_reader :method
9
+
10
+ def initialize(*)
11
+ super
12
+
13
+ @method = @opts.key?(:method) ? @opts[:method].to_s : "gzip"
14
+
15
+ raise Error, "Only gzip and deflate methods are supported" unless %w(gzip deflate).include?(@method)
16
+ end
17
+
18
+ def deflate(headers, body)
19
+ return body unless body
20
+ return body unless body.is_a?(String)
21
+
22
+ # We need to delete Content-Length header. It will be set automatically
23
+ # by HTTP::Request::Writer
24
+ headers.delete(Headers::CONTENT_LENGTH)
25
+
26
+ headers[Headers::CONTENT_ENCODING] = method
27
+
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
36
+ end
37
+ when "deflate" then
38
+ Zlib::Deflate.deflate(body)
39
+ else
40
+ raise ArgumentError, "Unsupported deflate method: #{method}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module HTTP
3
+ module Features
4
+ class AutoInflate < Feature
5
+ def stream_for(connection, response)
6
+ if %w(deflate gzip x-gzip).include?(response.headers[:content_encoding])
7
+ Response::Inflater.new(connection)
8
+ else
9
+ connection
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -68,6 +68,10 @@ module HTTP
68
68
  # Currently defined methods are: chunked, compress, deflate, gzip, identity.
69
69
  TRANSFER_ENCODING = "Transfer-Encoding".freeze
70
70
 
71
+ # Indicates what additional content codings have been applied to the
72
+ # entity-body.
73
+ CONTENT_ENCODING = "Content-Encoding".freeze
74
+
71
75
  # The user agent string of the user agent.
72
76
  USER_AGENT = "User-Agent".freeze
73
77
 
@@ -3,15 +3,24 @@ require "http/headers"
3
3
  require "openssl"
4
4
  require "socket"
5
5
  require "http/uri"
6
+ require "http/feature"
7
+ require "http/features/auto_inflate"
8
+ require "http/features/auto_deflate"
6
9
 
7
10
  module HTTP
11
+ # rubocop:disable Metrics/ClassLength
8
12
  class Options
9
13
  @default_socket_class = TCPSocket
10
14
  @default_ssl_socket_class = OpenSSL::SSL::SSLSocket
11
15
  @default_timeout_class = HTTP::Timeout::Null
16
+ @available_features = {
17
+ :auto_inflate => Features::AutoInflate,
18
+ :auto_deflate => Features::AutoDeflate
19
+ }
12
20
 
13
21
  class << self
14
22
  attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
23
+ attr_reader :available_features
15
24
 
16
25
  def new(options = {})
17
26
  return options if options.is_a?(self)
@@ -50,7 +59,8 @@ module HTTP
50
59
  :keep_alive_timeout => 5,
51
60
  :headers => {},
52
61
  :cookies => {},
53
- :encoding => nil
62
+ :encoding => nil,
63
+ :features => {}
54
64
  }
55
65
 
56
66
  opts_w_defaults = defaults.merge(options)
@@ -73,6 +83,38 @@ module HTTP
73
83
  self.encoding = Encoding.find(encoding)
74
84
  end
75
85
 
86
+ def_option :features do |features|
87
+ # Normalize features from:
88
+ #
89
+ # [{feature_one: {opt: 'val'}}, :feature_two]
90
+ #
91
+ # into:
92
+ #
93
+ # {feature_one: {opt: 'val'}, feature_two: {}}
94
+ features = features.each_with_object({}) do |feature, h|
95
+ if feature.is_a?(Hash)
96
+ h.merge!(feature)
97
+ else
98
+ h[feature] = {}
99
+ end
100
+ end
101
+
102
+ self.features.merge(features)
103
+ end
104
+
105
+ def features=(features)
106
+ @features = features.each_with_object({}) do |(name, opts_or_feature), h|
107
+ h[name] = if opts_or_feature.is_a?(Feature)
108
+ opts_or_feature
109
+ else
110
+ unless (feature = self.class.available_features[name])
111
+ argument_error! "Unsupported feature: #{name}"
112
+ end
113
+ feature.new(opts_or_feature)
114
+ end
115
+ end
116
+ end
117
+
76
118
  %w(
77
119
  proxy params form json body follow response
78
120
  socket_class nodelay ssl_socket_class ssl_context ssl
@@ -127,6 +169,10 @@ module HTTP
127
169
  dupped
128
170
  end
129
171
 
172
+ def feature(name)
173
+ features[name]
174
+ end
175
+
130
176
  protected
131
177
 
132
178
  def []=(option, val)
@@ -37,7 +37,10 @@ module HTTP
37
37
  # RFC 3744: WebDAV Access Control Protocol
38
38
  :acl,
39
39
 
40
- # draft-dusseault-http-patch: PATCH Method for HTTP
40
+ # RFC 6352: vCard Extensions to WebDAV -- CardDAV
41
+ :report,
42
+
43
+ # RFC 5789: PATCH Method for HTTP
41
44
  :patch,
42
45
 
43
46
  # draft-reschke-webdav-search: WebDAV Search
@@ -5,6 +5,7 @@ require "http/headers"
5
5
  require "http/content_type"
6
6
  require "http/mime_type"
7
7
  require "http/response/status"
8
+ require "http/response/inflater"
8
9
  require "http/uri"
9
10
  require "http/cookie_jar"
10
11
  require "time"
@@ -48,7 +49,9 @@ module HTTP
48
49
  connection = opts.fetch(:connection)
49
50
  encoding = opts[:encoding] || charset || Encoding::BINARY
50
51
 
51
- @body = Response::Body.new(connection, encoding)
52
+ stream = body_stream_for(connection, opts)
53
+
54
+ @body = Response::Body.new(connection, stream, encoding)
52
55
  else
53
56
  @body = opts.fetch(:body)
54
57
  end
@@ -143,5 +146,15 @@ module HTTP
143
146
  def inspect
144
147
  "#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
145
148
  end
149
+
150
+ private
151
+
152
+ def body_stream_for(connection, opts)
153
+ if opts[:auto_inflate]
154
+ opts[:auto_inflate].stream_for(connection, self)
155
+ else
156
+ connection
157
+ end
158
+ end
146
159
  end
147
160
  end
@@ -15,17 +15,18 @@ module HTTP
15
15
  # @return [HTTP::Connection]
16
16
  attr_reader :connection
17
17
 
18
- def initialize(connection, encoding = Encoding::BINARY)
18
+ def initialize(connection, stream, encoding = Encoding::BINARY)
19
19
  @connection = connection
20
20
  @streaming = nil
21
21
  @contents = nil
22
+ @stream = stream
22
23
  @encoding = encoding
23
24
  end
24
25
 
25
26
  # (see HTTP::Client#readpartial)
26
27
  def readpartial(*args)
27
28
  stream!
28
- @connection.readpartial(*args)
29
+ @stream.readpartial(*args)
29
30
  end
30
31
 
31
32
  # Iterate over the body, allowing it to be enumerable
@@ -52,7 +53,7 @@ module HTTP
52
53
  @streaming = false
53
54
  @contents = String.new("").force_encoding(encoding)
54
55
 
55
- while (chunk = @connection.readpartial)
56
+ while (chunk = @stream.readpartial)
56
57
  @contents << chunk.force_encoding(encoding)
57
58
  end
58
59
  rescue
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module HTTP
6
+ class Response
7
+ class Inflater
8
+ def initialize(connection)
9
+ @connection = connection
10
+ end
11
+
12
+ def readpartial(*args)
13
+ chunk = @connection.readpartial(*args)
14
+ if chunk
15
+ chunk = zstream.inflate(chunk)
16
+ elsif !zstream.closed?
17
+ zstream.finish
18
+ zstream.close
19
+ end
20
+ chunk
21
+ end
22
+
23
+ private
24
+
25
+ def zstream
26
+ @zstream ||= Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "2.1.0".freeze
4
+ VERSION = "2.2.0".freeze
5
5
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe HTTP::Features::AutoDeflate do
3
+ subject { HTTP::Features::AutoDeflate.new }
4
+
5
+ it "raises error for wrong type" do
6
+ expect { HTTP::Features::AutoDeflate.new(:method => :wrong) }.
7
+ to raise_error(HTTP::Error) { |error|
8
+ expect(error.message).to eq("Only gzip and deflate methods are supported")
9
+ }
10
+ end
11
+
12
+ it "accepts gzip method" do
13
+ expect(HTTP::Features::AutoDeflate.new(:method => :gzip).method).to eq "gzip"
14
+ end
15
+
16
+ it "accepts deflate method" do
17
+ expect(HTTP::Features::AutoDeflate.new(:method => :deflate).method).to eq "deflate"
18
+ end
19
+
20
+ it "accepts string as method" do
21
+ expect(HTTP::Features::AutoDeflate.new(:method => "gzip").method).to eq "gzip"
22
+ end
23
+
24
+ it "uses gzip by default" do
25
+ expect(subject.method).to eq("gzip")
26
+ end
27
+
28
+ describe "#deflate" do
29
+ let(:headers) { HTTP::Headers.coerce("Content-Length" => "10") }
30
+
31
+ context "when body is nil" do
32
+ let(:body) { nil }
33
+
34
+ it "returns nil" do
35
+ expect(subject.deflate(headers, body)).to be_nil
36
+ end
37
+
38
+ it "does not remove Content-Length header" do
39
+ subject.deflate(headers, body)
40
+ expect(headers["Content-Length"]).to eq "10"
41
+ end
42
+
43
+ it "does not set Content-Encoding header" do
44
+ subject.deflate(headers, body)
45
+ expect(headers.include?("Content-Encoding")).to eq false
46
+ end
47
+ end
48
+
49
+ context "when body is not a string" do
50
+ let(:body) { {} }
51
+
52
+ it "returns given body" do
53
+ expect(subject.deflate(headers, body).object_id).to eq(body.object_id)
54
+ end
55
+
56
+ it "does not remove Content-Length header" do
57
+ subject.deflate(headers, body)
58
+ expect(headers["Content-Length"]).to eq "10"
59
+ end
60
+
61
+ it "does not set Content-Encoding header" do
62
+ subject.deflate(headers, body)
63
+ expect(headers.include?("Content-Encoding")).to eq false
64
+ end
65
+ end
66
+
67
+ context "when body is a string" do
68
+ let(:body) { "Hello HTTP!" }
69
+
70
+ it "encodes body" do
71
+ encoded = subject.deflate(headers, body)
72
+ decoded = Zlib::GzipReader.new(StringIO.new(encoded)).read
73
+
74
+ expect(decoded).to eq(body)
75
+ end
76
+
77
+ it "removes Content-Length header" do
78
+ subject.deflate(headers, body)
79
+ expect(headers.include?("Content-Length")).to eq false
80
+ end
81
+
82
+ it "sets Content-Encoding header" do
83
+ subject.deflate(headers, body)
84
+ expect(headers["Content-Encoding"]).to eq "gzip"
85
+ end
86
+
87
+ context "as deflate method" do
88
+ subject { HTTP::Features::AutoDeflate.new(:method => :deflate) }
89
+
90
+ it "encodes body" do
91
+ encoded = subject.deflate(headers, body)
92
+ decoded = Zlib::Inflate.inflate(encoded)
93
+
94
+ expect(decoded).to eq(body)
95
+ end
96
+
97
+ it "removes Content-Length header" do
98
+ subject.deflate(headers, body)
99
+ expect(headers.include?("Content-Length")).to eq false
100
+ end
101
+
102
+ it "sets Content-Encoding header" do
103
+ subject.deflate(headers, body)
104
+ expect(headers["Content-Encoding"]).to eq "deflate"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe HTTP::Features::AutoInflate do
3
+ subject { HTTP::Features::AutoInflate.new }
4
+ let(:connection) { double }
5
+ let(:headers) { {} }
6
+ let(:response) do
7
+ HTTP::Response.new(
8
+ :version => "1.1",
9
+ :status => 200,
10
+ :headers => headers,
11
+ :connection => connection
12
+ )
13
+ end
14
+
15
+ describe "stream_for" do
16
+ context "when there is no Content-Encoding header" do
17
+ it "returns connection" do
18
+ stream = subject.stream_for(connection, response)
19
+ expect(stream).to eq(connection)
20
+ end
21
+ end
22
+
23
+ context "for identity Content-Encoding header" do
24
+ let(:headers) { {:content_encoding => "not-supported"} }
25
+
26
+ it "returns connection" do
27
+ stream = subject.stream_for(connection, response)
28
+ expect(stream).to eq(connection)
29
+ end
30
+ end
31
+
32
+ context "for unknown Content-Encoding header" do
33
+ let(:headers) { {:content_encoding => "not-supported"} }
34
+
35
+ it "returns connection" do
36
+ stream = subject.stream_for(connection, response)
37
+ expect(stream).to eq(connection)
38
+ end
39
+ end
40
+
41
+ context "for deflate Content-Encoding header" do
42
+ let(:headers) { {:content_encoding => "deflate"} }
43
+
44
+ it "returns HTTP::Response::Inflater instance - connection wrapper" do
45
+ stream = subject.stream_for(connection, response)
46
+ expect(stream).to be_instance_of HTTP::Response::Inflater
47
+ end
48
+ end
49
+
50
+ context "for gzip Content-Encoding header" do
51
+ let(:headers) { {:content_encoding => "gzip"} }
52
+
53
+ it "returns HTTP::Response::Inflater instance - connection wrapper" do
54
+ stream = subject.stream_for(connection, response)
55
+ expect(stream).to be_instance_of HTTP::Response::Inflater
56
+ end
57
+ end
58
+
59
+ context "for x-gzip Content-Encoding header" do
60
+ let(:headers) { {:content_encoding => "x-gzip"} }
61
+
62
+ it "returns HTTP::Response::Inflater instance - connection wrapper" do
63
+ stream = subject.stream_for(connection, response)
64
+ expect(stream).to be_instance_of HTTP::Response::Inflater
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe HTTP::Options, "features" do
3
+ let(:opts) { HTTP::Options.new }
4
+
5
+ it "defaults to be empty" do
6
+ expect(opts.features).to be_empty
7
+ end
8
+
9
+ it "accepts plain symbols in array" do
10
+ opts2 = opts.with_features([:auto_inflate])
11
+ expect(opts.features).to be_empty
12
+ expect(opts2.features.keys).to eq([:auto_inflate])
13
+ expect(opts2.features[:auto_inflate]).
14
+ to be_instance_of(HTTP::Features::AutoInflate)
15
+ end
16
+
17
+ it "accepts feature name with its options in array" do
18
+ opts2 = opts.with_features([{:auto_deflate => {:method => :deflate}}])
19
+ expect(opts.features).to be_empty
20
+ expect(opts2.features.keys).to eq([:auto_deflate])
21
+ expect(opts2.features[:auto_deflate]).
22
+ to be_instance_of(HTTP::Features::AutoDeflate)
23
+ expect(opts2.features[:auto_deflate].method).to eq("deflate")
24
+ end
25
+
26
+ it "raises error for not supported features" do
27
+ expect { opts.with_features([:wrong_feature]) }.
28
+ to raise_error(HTTP::Error) { |error|
29
+ expect(error.message).to eq("Unsupported feature: wrong_feature")
30
+ }
31
+ end
32
+ end
@@ -24,7 +24,8 @@ RSpec.describe HTTP::Options, "merge" do
24
24
  :body => "body-foo",
25
25
  :json => {:foo => "foo"},
26
26
  :headers => {:accept => "json", :foo => "foo"},
27
- :proxy => {}
27
+ :proxy => {},
28
+ :features => {}
28
29
  )
29
30
 
30
31
  bar = HTTP::Options.new(
@@ -60,7 +61,8 @@ RSpec.describe HTTP::Options, "merge" do
60
61
  :ssl_socket_class => described_class.default_ssl_socket_class,
61
62
  :ssl_context => nil,
62
63
  :cookies => {},
63
- :encoding => nil
64
+ :encoding => nil,
65
+ :features => {}
64
66
  )
65
67
  end
66
68
  end
@@ -5,7 +5,7 @@ RSpec.describe HTTP::Response::Body do
5
5
 
6
6
  before { allow(connection).to receive(:readpartial) { chunks.shift } }
7
7
 
8
- subject(:body) { described_class.new(connection, Encoding::UTF_8) }
8
+ subject(:body) { described_class.new(connection, connection, Encoding::UTF_8) }
9
9
 
10
10
  it "streams bodies from responses" do
11
11
  expect(subject.to_s).to eq("Hello, World!")
@@ -38,4 +38,32 @@ RSpec.describe HTTP::Response::Body do
38
38
  end
39
39
  end
40
40
  end
41
+
42
+ context "when body is gzipped" do
43
+ let(:chunks) do
44
+ body = Zlib::Deflate.deflate("Hi, HTTP here ☺")
45
+ len = body.length
46
+ [String.new(body[0, len / 2]), String.new(body[(len / 2)..-1])]
47
+ end
48
+ subject(:body) do
49
+ inflater = HTTP::Response::Inflater.new(connection)
50
+ described_class.new(connection, inflater, Encoding::UTF_8)
51
+ end
52
+
53
+ it "decodes body" do
54
+ expect(subject.to_s).to eq("Hi, HTTP here ☺")
55
+ end
56
+
57
+ describe "#readpartial" do
58
+ it "streams decoded body" do
59
+ [
60
+ "Hi, HTTP ",
61
+ String.new("here ☺").force_encoding("ASCII-8BIT"),
62
+ nil
63
+ ].each do |part|
64
+ expect(subject.readpartial).to eq(part)
65
+ end
66
+ end
67
+ end
68
+ end
41
69
  end
@@ -410,4 +410,47 @@ RSpec.describe HTTP do
410
410
  expect(socket_spy_class.setsockopt_calls).to eq([[Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1]])
411
411
  end
412
412
  end
413
+
414
+ describe ".use" do
415
+ it "turns on given feature" do
416
+ client = HTTP.use :auto_deflate
417
+ expect(client.default_options.features.keys).to eq [:auto_deflate]
418
+ end
419
+
420
+ context "with :auto_deflate" do
421
+ it "sends gzipped body" do
422
+ client = HTTP.use :auto_deflate
423
+ body = "Hello!"
424
+ response = client.post("#{dummy.endpoint}/echo-body", :body => body)
425
+ encoded = response.to_s
426
+
427
+ expect(Zlib::GzipReader.new(StringIO.new(encoded)).read).to eq body
428
+ end
429
+ end
430
+
431
+ context "with :auto_inflate" do
432
+ it "returns raw body when Content-Encoding type is missing" do
433
+ client = HTTP.use :auto_inflate
434
+ body = "Hello!"
435
+ response = client.post("#{dummy.endpoint}/encoded-body", :body => body)
436
+ expect(response.to_s).to eq("#{body}-raw")
437
+ end
438
+
439
+ it "returns decoded body" do
440
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
441
+ body = "Hello!"
442
+ response = client.post("#{dummy.endpoint}/encoded-body", :body => body)
443
+
444
+ expect(response.to_s).to eq("#{body}-gzipped")
445
+ end
446
+
447
+ it "returns deflated body" do
448
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
449
+ body = "Hello!"
450
+ response = client.post("#{dummy.endpoint}/encoded-body", :body => body)
451
+
452
+ expect(response.to_s).to eq("#{body}-deflated")
453
+ end
454
+ end
455
+ end
413
456
  end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # coding: utf-8
3
2
 
4
3
  require "simplecov"
5
4
  require "coveralls"
@@ -20,11 +19,6 @@ require "http"
20
19
  require "rspec/its"
21
20
  require "support/capture_warning"
22
21
 
23
- # Are we in a flaky environment?
24
- def flaky_env?
25
- defined?(JRUBY_VERSION) && ENV["CI"]
26
- end
27
-
28
22
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
29
23
  RSpec.configure do |config|
30
24
  config.expect_with :rspec do |expectations|
@@ -50,6 +44,7 @@ RSpec.configure do |config|
50
44
  # `:focus` metadata. When nothing is tagged with `:focus`, all examples
51
45
  # get run.
52
46
  config.filter_run :focus
47
+ config.filter_run_excluding :flaky if defined?(JRUBY_VERSION) && ENV["CI"]
53
48
  config.run_all_when_everything_filtered = true
54
49
 
55
50
  # Limits the available syntax to the non-monkey patched syntax that is recommended.
@@ -2,6 +2,7 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  class DummyServer < WEBrick::HTTPServer
5
+ # rubocop:disable Metrics/ClassLength
5
6
  class Servlet < WEBrick::HTTPServlet::AbstractServlet
6
7
  def self.sockets
7
8
  @sockets ||= []
@@ -146,5 +147,26 @@ class DummyServer < WEBrick::HTTPServer
146
147
  res.status = 200
147
148
  res.body = req.body
148
149
  end
150
+
151
+ post "/encoded-body" do |req, res|
152
+ res.status = 200
153
+
154
+ res.body = case req["Accept-Encoding"]
155
+ when "gzip" then
156
+ res["Content-Encoding"] = "gzip"
157
+ StringIO.open do |out|
158
+ Zlib::GzipWriter.wrap(out) do |gz|
159
+ gz.write "#{req.body}-gzipped"
160
+ gz.finish
161
+ out.tap(&:rewind).read
162
+ end
163
+ end
164
+ when "deflate" then
165
+ res["Content-Encoding"] = "deflate"
166
+ Zlib::Deflate.deflate("#{req.body}-deflated")
167
+ else
168
+ "#{req.body}-raw"
169
+ end
170
+ end
149
171
  end
150
172
  end
@@ -50,8 +50,7 @@ RSpec.shared_context "HTTP handling" do
50
50
  context "of 0" do
51
51
  let(:read_timeout) { 0 }
52
52
 
53
- it "times out" do
54
- skip "flaky environment" if flaky_env?
53
+ it "times out", :flaky do
55
54
  expect { response }.to raise_error(HTTP::TimeoutError, /Read/i)
56
55
  end
57
56
  end
@@ -59,10 +58,7 @@ RSpec.shared_context "HTTP handling" do
59
58
  context "of 2.5" do
60
59
  let(:read_timeout) { 2.5 }
61
60
 
62
- it "does not time out" do
63
- # TODO: investigate sporadic JRuby timeouts on CI
64
- skip "flaky environment" if flaky_env?
65
-
61
+ it "does not time out", :flaky do
66
62
  expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error
67
63
  end
68
64
  end
@@ -96,10 +92,7 @@ RSpec.shared_context "HTTP handling" do
96
92
 
97
93
  let(:read_timeout) { 2.5 }
98
94
 
99
- it "does not timeout" do
100
- # TODO: investigate sporadic JRuby timeouts on CI
101
- skip "flaky environment" if flaky_env?
102
-
95
+ it "does not timeout", :flaky do
103
96
  client.get("#{server.endpoint}/sleep").body.to_s
104
97
  client.get("#{server.endpoint}/sleep").body.to_s
105
98
  end
@@ -130,9 +123,7 @@ RSpec.shared_context "HTTP handling" do
130
123
  end
131
124
 
132
125
  context "on a mixed state" do
133
- it "re-opens the connection" do
134
- skip "flaky environment" if flaky_env?
135
-
126
+ it "re-opens the connection", :flaky do
136
127
  first_socket_id = client.get("#{server.endpoint}/socket/1").body.to_s
137
128
 
138
129
  client.instance_variable_set(:@state, :dirty)
@@ -163,9 +154,7 @@ RSpec.shared_context "HTTP handling" do
163
154
  end
164
155
 
165
156
  context "with a socket issue" do
166
- it "transparently reopens" do
167
- skip "flaky environment" if flaky_env?
168
-
157
+ it "transparently reopens", :flaky do
169
158
  first_socket_id = client.get("#{server.endpoint}/socket").body.to_s
170
159
  expect(first_socket_id).to_not eq("")
171
160
  # Kill off the sockets we used
@@ -195,9 +184,7 @@ RSpec.shared_context "HTTP handling" do
195
184
  context "when disabled" do
196
185
  let(:options) { {} }
197
186
 
198
- it "opens new sockets" do
199
- skip "flaky environment" if flaky_env?
200
-
187
+ it "opens new sockets", :flaky do
201
188
  expect(sockets_used).to_not include("")
202
189
  expect(sockets_used.uniq.length).to eq(2)
203
190
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2016-11-09 00:00:00.000000000 Z
14
+ date: 2017-02-03 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: http_parser.rb
@@ -95,6 +95,7 @@ files:
95
95
  - ".gitignore"
96
96
  - ".rspec"
97
97
  - ".rubocop.yml"
98
+ - ".ruby-version"
98
99
  - ".travis.yml"
99
100
  - ".yardopts"
100
101
  - CHANGES.md
@@ -111,6 +112,9 @@ files:
111
112
  - lib/http/connection.rb
112
113
  - lib/http/content_type.rb
113
114
  - lib/http/errors.rb
115
+ - lib/http/feature.rb
116
+ - lib/http/features/auto_deflate.rb
117
+ - lib/http/features/auto_inflate.rb
114
118
  - lib/http/headers.rb
115
119
  - lib/http/headers/known.rb
116
120
  - lib/http/headers/mixin.rb
@@ -123,6 +127,7 @@ files:
123
127
  - lib/http/request/writer.rb
124
128
  - lib/http/response.rb
125
129
  - lib/http/response/body.rb
130
+ - lib/http/response/inflater.rb
126
131
  - lib/http/response/parser.rb
127
132
  - lib/http/response/status.rb
128
133
  - lib/http/response/status/reasons.rb
@@ -134,9 +139,12 @@ files:
134
139
  - logo.png
135
140
  - spec/lib/http/client_spec.rb
136
141
  - spec/lib/http/content_type_spec.rb
142
+ - spec/lib/http/features/auto_deflate_spec.rb
143
+ - spec/lib/http/features/auto_inflate_spec.rb
137
144
  - spec/lib/http/headers/mixin_spec.rb
138
145
  - spec/lib/http/headers_spec.rb
139
146
  - spec/lib/http/options/body_spec.rb
147
+ - spec/lib/http/options/features_spec.rb
140
148
  - spec/lib/http/options/form_spec.rb
141
149
  - spec/lib/http/options/headers_spec.rb
142
150
  - spec/lib/http/options/json_spec.rb
@@ -183,16 +191,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
191
  version: '0'
184
192
  requirements: []
185
193
  rubyforge_project:
186
- rubygems_version: 2.5.1
194
+ rubygems_version: 2.6.8
187
195
  signing_key:
188
196
  specification_version: 4
189
197
  summary: HTTP should be easy
190
198
  test_files:
191
199
  - spec/lib/http/client_spec.rb
192
200
  - spec/lib/http/content_type_spec.rb
201
+ - spec/lib/http/features/auto_deflate_spec.rb
202
+ - spec/lib/http/features/auto_inflate_spec.rb
193
203
  - spec/lib/http/headers/mixin_spec.rb
194
204
  - spec/lib/http/headers_spec.rb
195
205
  - spec/lib/http/options/body_spec.rb
206
+ - spec/lib/http/options/features_spec.rb
196
207
  - spec/lib/http/options/form_spec.rb
197
208
  - spec/lib/http/options/headers_spec.rb
198
209
  - spec/lib/http/options/json_spec.rb