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 +4 -4
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -0
- data/.travis.yml +12 -11
- data/CHANGES.md +12 -0
- data/Gemfile +2 -2
- data/README.md +1 -1
- data/lib/http/chainable.rb +12 -2
- data/lib/http/client.rb +19 -12
- data/lib/http/feature.rb +8 -0
- data/lib/http/features/auto_deflate.rb +45 -0
- data/lib/http/features/auto_inflate.rb +14 -0
- data/lib/http/headers/known.rb +4 -0
- data/lib/http/options.rb +47 -1
- data/lib/http/request.rb +4 -1
- data/lib/http/response.rb +14 -1
- data/lib/http/response/body.rb +4 -3
- data/lib/http/response/inflater.rb +30 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/features/auto_deflate_spec.rb +109 -0
- data/spec/lib/http/features/auto_inflate_spec.rb +68 -0
- data/spec/lib/http/options/features_spec.rb +32 -0
- data/spec/lib/http/options/merge_spec.rb +4 -2
- data/spec/lib/http/response/body_spec.rb +29 -1
- data/spec/lib/http_spec.rb +43 -0
- data/spec/spec_helper.rb +1 -6
- data/spec/support/dummy_server/servlet.rb +22 -0
- data/spec/support/http_handling_shared.rb +6 -19
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 516d371c9aa3a19abeaabeefea99bd16be506f36
|
4
|
+
data.tar.gz: 030d18563e33fc3aad5832cb98508013cba1a956
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33f746cb66e0493bb3ca2a272bbf7364a6a949e804dc7f581738a05b373367b08baf0798e838e3a7865d7ef782f0e74dd823204a63bb97b7f334964673e9ed0f
|
7
|
+
data.tar.gz: c426e9ff531bbc8268b7bfd3d49276c0fb890e063a69fc3fd6000c8580631dfb62fd83d04088c1a77ed246078fe77c30364c42cdaa527a1f2cedcb1e55499c77
|
data/.rubocop.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.0
|
data/.travis.yml
CHANGED
@@ -1,28 +1,29 @@
|
|
1
1
|
language: ruby
|
2
2
|
sudo: false
|
3
3
|
|
4
|
-
|
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.
|
15
|
-
- 2.
|
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",
|
26
|
+
gem "activemodel", :require => false # Used by certificate_authority
|
27
27
|
end
|
28
28
|
|
29
29
|
group :doc do
|
data/README.md
CHANGED
data/lib/http/chainable.rb
CHANGED
@@ -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:
|
data/lib/http/client.rb
CHANGED
@@ -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
|
-
|
154
|
-
|
155
|
-
opts.body
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
data/lib/http/feature.rb
ADDED
@@ -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
|
data/lib/http/headers/known.rb
CHANGED
@@ -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
|
|
data/lib/http/options.rb
CHANGED
@@ -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)
|
data/lib/http/request.rb
CHANGED
@@ -37,7 +37,10 @@ module HTTP
|
|
37
37
|
# RFC 3744: WebDAV Access Control Protocol
|
38
38
|
:acl,
|
39
39
|
|
40
|
-
#
|
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
|
data/lib/http/response.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/http/response/body.rb
CHANGED
@@ -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
|
-
@
|
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 = @
|
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
|
data/lib/http/version.rb
CHANGED
@@ -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
|
data/spec/lib/http_spec.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
@@ -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.
|
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:
|
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.
|
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
|