http 2.1.0 → 2.2.0

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