http 0.7.4 → 0.8.0.pre
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/.rspec +0 -1
- data/.rubocop.yml +5 -2
- data/CHANGES.md +24 -7
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +24 -22
- data/Guardfile +2 -2
- data/README.md +34 -4
- data/Rakefile +7 -7
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +12 -12
- data/lib/http.rb +11 -10
- data/lib/http/cache.rb +146 -0
- data/lib/http/cache/headers.rb +100 -0
- data/lib/http/cache/null_cache.rb +13 -0
- data/lib/http/chainable.rb +14 -3
- data/lib/http/client.rb +64 -80
- data/lib/http/connection.rb +139 -0
- data/lib/http/content_type.rb +2 -2
- data/lib/http/errors.rb +7 -1
- data/lib/http/headers.rb +21 -8
- data/lib/http/headers/mixin.rb +1 -1
- data/lib/http/mime_type.rb +2 -2
- data/lib/http/mime_type/adapter.rb +2 -2
- data/lib/http/mime_type/json.rb +4 -4
- data/lib/http/options.rb +65 -74
- data/lib/http/redirector.rb +3 -3
- data/lib/http/request.rb +20 -13
- data/lib/http/request/caching.rb +95 -0
- data/lib/http/request/writer.rb +5 -5
- data/lib/http/response.rb +15 -9
- data/lib/http/response/body.rb +21 -8
- data/lib/http/response/caching.rb +142 -0
- data/lib/http/response/io_body.rb +63 -0
- data/lib/http/response/parser.rb +1 -1
- data/lib/http/response/status.rb +4 -12
- data/lib/http/response/status/reasons.rb +53 -53
- data/lib/http/response/string_body.rb +53 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/cache/headers_spec.rb +77 -0
- data/spec/lib/http/cache_spec.rb +182 -0
- data/spec/lib/http/client_spec.rb +123 -95
- data/spec/lib/http/content_type_spec.rb +25 -25
- data/spec/lib/http/headers/mixin_spec.rb +8 -8
- data/spec/lib/http/headers_spec.rb +213 -173
- data/spec/lib/http/options/body_spec.rb +5 -5
- data/spec/lib/http/options/form_spec.rb +3 -3
- data/spec/lib/http/options/headers_spec.rb +7 -7
- data/spec/lib/http/options/json_spec.rb +3 -3
- data/spec/lib/http/options/merge_spec.rb +26 -22
- data/spec/lib/http/options/new_spec.rb +10 -10
- data/spec/lib/http/options/proxy_spec.rb +8 -8
- data/spec/lib/http/options_spec.rb +2 -2
- data/spec/lib/http/redirector_spec.rb +32 -32
- data/spec/lib/http/request/caching_spec.rb +133 -0
- data/spec/lib/http/request/writer_spec.rb +26 -26
- data/spec/lib/http/request_spec.rb +63 -58
- data/spec/lib/http/response/body_spec.rb +13 -13
- data/spec/lib/http/response/caching_spec.rb +201 -0
- data/spec/lib/http/response/io_body_spec.rb +35 -0
- data/spec/lib/http/response/status_spec.rb +25 -25
- data/spec/lib/http/response/string_body_spec.rb +35 -0
- data/spec/lib/http/response_spec.rb +64 -45
- data/spec/lib/http_spec.rb +103 -76
- data/spec/spec_helper.rb +10 -12
- data/spec/support/connection_reuse_shared.rb +100 -0
- data/spec/support/create_certs.rb +12 -12
- data/spec/support/dummy_server.rb +11 -11
- data/spec/support/dummy_server/servlet.rb +43 -31
- data/spec/support/proxy_server.rb +31 -25
- metadata +57 -8
- data/spec/support/example_server.rb +0 -30
- data/spec/support/example_server/servlet.rb +0 -102
@@ -0,0 +1,53 @@
|
|
1
|
+
module HTTP
|
2
|
+
class Response
|
3
|
+
# A Body class that wraps a String, rather than a the client
|
4
|
+
# object.
|
5
|
+
class StringBody
|
6
|
+
include Enumerable
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
# @return [String,nil] the next `size` octets part of the
|
10
|
+
# body, or nil if whole body has already been read.
|
11
|
+
def readpartial(size = @contents.length)
|
12
|
+
stream!
|
13
|
+
return nil if @streaming_offset >= @contents.length
|
14
|
+
|
15
|
+
@contents[@streaming_offset, size].tap do |part|
|
16
|
+
@streaming_offset += (part.length + 1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Iterate over the body, allowing it to be enumerable
|
21
|
+
def each
|
22
|
+
yield @contents
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String] eagerly consume the entire body as a string
|
26
|
+
def to_s
|
27
|
+
@contents
|
28
|
+
end
|
29
|
+
alias_method :to_str, :to_s
|
30
|
+
|
31
|
+
def_delegator :@contents, :empty?
|
32
|
+
|
33
|
+
# Assert that the body is actively being streamed
|
34
|
+
def stream!
|
35
|
+
fail StateError, "body has already been consumed" if @streaming == false
|
36
|
+
@streaming = true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Easier to interpret string inspect
|
40
|
+
def inspect
|
41
|
+
"#<#{self.class}:#{object_id.to_s(16)}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def initialize(contents)
|
47
|
+
@contents = contents
|
48
|
+
@streaming = nil
|
49
|
+
@streaming_offset = 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/http/version.rb
CHANGED
@@ -0,0 +1,77 @@
|
|
1
|
+
RSpec.describe HTTP::Cache::Headers do
|
2
|
+
subject(:cache_headers) { described_class.new headers }
|
3
|
+
|
4
|
+
describe ".new" do
|
5
|
+
it "accepts instance of HTTP::Headers" do
|
6
|
+
expect { described_class.new HTTP::Headers.new }.not_to raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
it "it rejects any object that does not respond to #headers" do
|
10
|
+
expect { described_class.new double }.to raise_error HTTP::Error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with <Cache-Control: private>" do
|
15
|
+
let(:headers) { {"Cache-Control" => "private"} }
|
16
|
+
it { is_expected.to be_private }
|
17
|
+
end
|
18
|
+
|
19
|
+
context "with <Cache-Control: public>" do
|
20
|
+
let(:headers) { {"Cache-Control" => "public"} }
|
21
|
+
it { is_expected.to be_public }
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with <Cache-Control: no-cache>" do
|
25
|
+
let(:headers) { {"Cache-Control" => "no-cache"} }
|
26
|
+
it { is_expected.to be_no_cache }
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with <Cache-Control: no-store>" do
|
30
|
+
let(:headers) { {"Cache-Control" => "no-store"} }
|
31
|
+
it { is_expected.to be_no_store }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#max_age" do
|
35
|
+
subject { cache_headers.max_age }
|
36
|
+
|
37
|
+
context "with <Cache-Control: max-age=100>" do
|
38
|
+
let(:headers) { {"Cache-Control" => "max-age=100"} }
|
39
|
+
it { is_expected.to eq 100 }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with <Expires: {100 seconds from now}>" do
|
43
|
+
let(:headers) { {"Expires" => (Time.now + 100).httpdate} }
|
44
|
+
it { is_expected.to be_within(1).of(100) }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with <Expires: {100 seconds before now}>" do
|
48
|
+
let(:headers) { {"Expires" => (Time.now - 100).httpdate} }
|
49
|
+
it { is_expected.to eq 0 }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with <Expires: -1>" do
|
53
|
+
let(:headers) { {"Expires" => "-1"} }
|
54
|
+
it { is_expected.to eq 0 }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with <Vary: *>" do
|
59
|
+
let(:headers) { {"Vary" => "*"} }
|
60
|
+
it { is_expected.to be_vary_star }
|
61
|
+
end
|
62
|
+
|
63
|
+
context "with no cache related headers" do
|
64
|
+
let(:headers) { {} }
|
65
|
+
|
66
|
+
it { is_expected.not_to be_private }
|
67
|
+
it { is_expected.not_to be_public }
|
68
|
+
it { is_expected.not_to be_no_cache }
|
69
|
+
it { is_expected.not_to be_no_store }
|
70
|
+
it { is_expected.not_to be_vary_star }
|
71
|
+
|
72
|
+
describe "#max_age" do
|
73
|
+
subject { cache_headers.max_age }
|
74
|
+
it { is_expected.to eq Float::INFINITY }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require "support/dummy_server"
|
2
|
+
require "http/cache"
|
3
|
+
|
4
|
+
RSpec.describe HTTP::Cache do
|
5
|
+
describe "creation" do
|
6
|
+
subject { described_class }
|
7
|
+
|
8
|
+
it "allows metastore and entitystore" do
|
9
|
+
expect(subject.new(:metastore => "heap:/", :entitystore => "heap:/"))
|
10
|
+
.to be_kind_of HTTP::Cache
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:opts) { options }
|
15
|
+
let(:sn) { SecureRandom.urlsafe_base64(3) }
|
16
|
+
let(:request) { HTTP::Request.new(:get, "http://example.com/#{sn}") }
|
17
|
+
|
18
|
+
let(:origin_response) do
|
19
|
+
HTTP::Response.new(200,
|
20
|
+
"http/1.1",
|
21
|
+
{"Cache-Control" => "private"},
|
22
|
+
"origin")
|
23
|
+
end
|
24
|
+
|
25
|
+
subject { described_class.new(:metastore => "heap:/", :entitystore => "heap:/") }
|
26
|
+
|
27
|
+
describe "#perform" do
|
28
|
+
it "calls request_performer blocck when cache miss" do
|
29
|
+
expect do |b|
|
30
|
+
subject.perform(request, opts) do |*args|
|
31
|
+
b.to_proc.call(*args)
|
32
|
+
origin_response
|
33
|
+
end
|
34
|
+
end.to yield_with_args(request, opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
context "cache hit" do
|
38
|
+
it "does not call request_performer block" do
|
39
|
+
subject.perform(request, opts) do |*_t|
|
40
|
+
origin_response
|
41
|
+
end
|
42
|
+
|
43
|
+
expect { |b| subject.perform(request, opts, &b) }.not_to yield_control
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "empty cache, cacheable request, cacheable response" do
|
49
|
+
let!(:response) { subject.perform(request, opts) { origin_response } }
|
50
|
+
|
51
|
+
it "returns origin servers response" do
|
52
|
+
expect(response).to eq origin_response
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "cache by-passing request, cacheable response" do
|
57
|
+
let(:request) do
|
58
|
+
headers = {"Cache-Control" => "no-cache"}
|
59
|
+
HTTP::Request.new(:get, "http://example.com/", headers)
|
60
|
+
end
|
61
|
+
let!(:response) { subject.perform(request, opts) { origin_response } }
|
62
|
+
|
63
|
+
it "returns origin servers response" do
|
64
|
+
expect(response).to eq origin_response
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "empty cache, cacheable request, 'no-cache' response" do
|
69
|
+
let(:origin_response) do
|
70
|
+
HTTP::Response.new(200,
|
71
|
+
"http/1.1",
|
72
|
+
{"Cache-Control" => "no-store"},
|
73
|
+
"")
|
74
|
+
end
|
75
|
+
let!(:response) { subject.perform(request, opts) { origin_response } }
|
76
|
+
|
77
|
+
it "returns origin servers response" do
|
78
|
+
expect(response).to eq origin_response
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "empty cache, cacheable request, 'no-store' response" do
|
83
|
+
let(:origin_response) do
|
84
|
+
HTTP::Response.new(200,
|
85
|
+
"http/1.1",
|
86
|
+
{"Cache-Control" => "no-store"},
|
87
|
+
"")
|
88
|
+
end
|
89
|
+
let!(:response) { subject.perform(request, opts) { origin_response } }
|
90
|
+
|
91
|
+
it "returns origin servers response" do
|
92
|
+
expect(response).to eq origin_response
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "warm cache, cacheable request, cacheable response" do
|
97
|
+
let(:cached_response) do
|
98
|
+
build_cached_response(200,
|
99
|
+
"1.1",
|
100
|
+
{"Cache-Control" => "max-age=100"},
|
101
|
+
"cached")
|
102
|
+
end
|
103
|
+
before do
|
104
|
+
subject.perform(request, opts) { cached_response }
|
105
|
+
end
|
106
|
+
|
107
|
+
let(:response) { subject.perform(request, opts) { origin_response } }
|
108
|
+
|
109
|
+
it "returns cached response" do
|
110
|
+
expect(response.body.to_s).to eq cached_response.body.to_s
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "stale cache, cacheable request, cacheable response" do
|
115
|
+
let(:cached_response) do
|
116
|
+
build_cached_response(200,
|
117
|
+
"1.1",
|
118
|
+
{"Cache-Control" => "private, max-age=1",
|
119
|
+
"Date" => (Time.now - 2).httpdate},
|
120
|
+
"cached") do |t|
|
121
|
+
t.requested_at = (Time.now - 2)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
before do
|
125
|
+
subject.perform(request, opts) { cached_response }
|
126
|
+
end
|
127
|
+
let(:response) { subject.perform(request, opts) { origin_response } }
|
128
|
+
|
129
|
+
it "returns origin servers response" do
|
130
|
+
expect(response.body.to_s).to eq origin_response.body.to_s
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "stale cache, cacheable request, not modified response" do
|
135
|
+
let(:cached_response) do
|
136
|
+
build_cached_response(200,
|
137
|
+
"http/1.1",
|
138
|
+
{"Cache-Control" => "private, max-age=1",
|
139
|
+
"Etag" => "foo",
|
140
|
+
"Date" => (Time.now - 2).httpdate},
|
141
|
+
"") do |x|
|
142
|
+
x.requested_at = (Time.now - 2)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
before do
|
146
|
+
subject.perform(request, opts) { cached_response }
|
147
|
+
end
|
148
|
+
|
149
|
+
let(:origin_response) { HTTP::Response.new(304, "http/1.1", {}, "") }
|
150
|
+
let(:response) { subject.perform(request, opts) { origin_response } }
|
151
|
+
|
152
|
+
it "makes request with conditional request headers" do
|
153
|
+
subject.perform(request, opts) do |actual_request, _|
|
154
|
+
expect(actual_request.headers["If-None-Match"])
|
155
|
+
.to eq cached_response.headers["Etag"]
|
156
|
+
expect(actual_request.headers["If-Modified-Since"])
|
157
|
+
.to eq cached_response.headers["Last-Modified"]
|
158
|
+
|
159
|
+
origin_response
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it "returns cached servers response" do
|
164
|
+
expect(response.body.to_s).to eq cached_response.body.to_s
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
let(:cached_response) { nil } # cold cache by default
|
169
|
+
|
170
|
+
def build_cached_response(*args)
|
171
|
+
r = HTTP::Response.new(*args).caching
|
172
|
+
r.requested_at = r.received_at = Time.now
|
173
|
+
|
174
|
+
yield r if block_given?
|
175
|
+
|
176
|
+
r
|
177
|
+
end
|
178
|
+
|
179
|
+
def options
|
180
|
+
HTTP::Options.new
|
181
|
+
end
|
182
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
|
-
require
|
1
|
+
require "support/connection_reuse_shared"
|
2
|
+
require "support/dummy_server"
|
3
|
+
require "http/cache"
|
2
4
|
|
3
5
|
RSpec.describe HTTP::Client do
|
4
|
-
let(:test_endpoint) { "http://#{ExampleServer::ADDR}" }
|
5
6
|
run_server(:dummy) { DummyServer.new }
|
6
7
|
run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
|
7
8
|
|
8
9
|
StubbedClient = Class.new(HTTP::Client) do
|
9
|
-
def
|
10
|
+
def make_request(request, options)
|
10
11
|
stubs.fetch(request.uri.to_s) { super(request, options) }
|
11
12
|
end
|
12
13
|
|
@@ -21,105 +22,125 @@ RSpec.describe HTTP::Client do
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def redirect_response(location, status = 302)
|
24
|
-
HTTP::Response.new(status,
|
25
|
+
HTTP::Response.new(status, "1.1", {"Location" => location}, "")
|
25
26
|
end
|
26
27
|
|
27
28
|
def simple_response(body, status = 200)
|
28
|
-
HTTP::Response.new(status,
|
29
|
+
HTTP::Response.new(status, "1.1", {}, body)
|
29
30
|
end
|
30
31
|
|
31
|
-
describe
|
32
|
-
it
|
32
|
+
describe "following redirects" do
|
33
|
+
it "returns response of new location" do
|
33
34
|
client = StubbedClient.new(:follow => true).stub(
|
34
|
-
|
35
|
-
|
35
|
+
"http://example.com/" => redirect_response("http://example.com/blog"),
|
36
|
+
"http://example.com/blog" => simple_response("OK")
|
36
37
|
)
|
37
38
|
|
38
|
-
expect(client.get(
|
39
|
+
expect(client.get("http://example.com/").to_s).to eq "OK"
|
39
40
|
end
|
40
41
|
|
41
|
-
it
|
42
|
+
it "prepends previous request uri scheme and host if needed" do
|
42
43
|
client = StubbedClient.new(:follow => true).stub(
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
"http://example.com/" => redirect_response("/index"),
|
45
|
+
"http://example.com/index" => redirect_response("/index.html"),
|
46
|
+
"http://example.com/index.html" => simple_response("OK")
|
46
47
|
)
|
47
48
|
|
48
|
-
expect(client.get(
|
49
|
+
expect(client.get("http://example.com/").to_s).to eq "OK"
|
49
50
|
end
|
50
51
|
|
51
|
-
it
|
52
|
+
it "fails upon endless redirects" do
|
52
53
|
client = StubbedClient.new(:follow => true).stub(
|
53
|
-
|
54
|
+
"http://example.com/" => redirect_response("/")
|
54
55
|
)
|
55
56
|
|
56
|
-
expect { client.get(
|
57
|
+
expect { client.get("http://example.com/") }
|
57
58
|
.to raise_error(HTTP::Redirector::EndlessRedirectError)
|
58
59
|
end
|
59
60
|
|
60
|
-
it
|
61
|
+
it "fails if max amount of hops reached" do
|
61
62
|
client = StubbedClient.new(:follow => 5).stub(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
63
|
+
"http://example.com/" => redirect_response("/1"),
|
64
|
+
"http://example.com/1" => redirect_response("/2"),
|
65
|
+
"http://example.com/2" => redirect_response("/3"),
|
66
|
+
"http://example.com/3" => redirect_response("/4"),
|
67
|
+
"http://example.com/4" => redirect_response("/5"),
|
68
|
+
"http://example.com/5" => redirect_response("/6"),
|
69
|
+
"http://example.com/6" => simple_response("OK")
|
69
70
|
)
|
70
71
|
|
71
|
-
expect { client.get(
|
72
|
+
expect { client.get("http://example.com/") }
|
72
73
|
.to raise_error(HTTP::Redirector::TooManyRedirectsError)
|
73
74
|
end
|
74
75
|
end
|
75
76
|
|
76
|
-
describe
|
77
|
+
describe "caching" do
|
78
|
+
let(:sn) { SecureRandom.urlsafe_base64(3) }
|
79
|
+
|
80
|
+
it "returns cached responses if they exist" do
|
81
|
+
cached_response = simple_response("cached").caching
|
82
|
+
StubbedClient.new(:cache =>
|
83
|
+
HTTP::Cache.new(:metastore => "heap:/", :entitystore => "heap:/"))
|
84
|
+
.stub("http://example.com/#{sn}" => cached_response)
|
85
|
+
.get("http://example.com/#{sn}")
|
86
|
+
|
87
|
+
# cache is now warm
|
88
|
+
|
89
|
+
client = StubbedClient.new(:cache => HTTP::Cache.new(:metastore => "heap:/", :entitystore => "heap:/"))
|
90
|
+
.stub("http://example.com/#{sn}" => simple_response("OK"))
|
91
|
+
|
92
|
+
expect(client.get("http://example.com/#{sn}").body.to_s)
|
93
|
+
.to eq cached_response.body.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "parsing params" do
|
77
98
|
let(:client) { HTTP::Client.new }
|
78
99
|
before { allow(client).to receive :perform }
|
79
100
|
|
80
|
-
it
|
101
|
+
it "accepts params within the provided URL" do
|
81
102
|
expect(HTTP::Request).to receive(:new) do |_, uri|
|
82
|
-
expect(CGI.parse uri.query).to eq(
|
103
|
+
expect(CGI.parse uri.query).to eq("foo" => %w(bar))
|
83
104
|
end
|
84
105
|
|
85
|
-
client.get(
|
106
|
+
client.get("http://example.com/?foo=bar")
|
86
107
|
end
|
87
108
|
|
88
|
-
it
|
109
|
+
it "combines GET params from the URI with the passed in params" do
|
89
110
|
expect(HTTP::Request).to receive(:new) do |_, uri|
|
90
|
-
expect(CGI.parse uri.query).to eq(
|
111
|
+
expect(CGI.parse uri.query).to eq("foo" => %w(bar), "baz" => %w(quux))
|
91
112
|
end
|
92
113
|
|
93
|
-
client.get(
|
114
|
+
client.get("http://example.com/?foo=bar", :params => {:baz => "quux"})
|
94
115
|
end
|
95
116
|
|
96
|
-
it
|
117
|
+
it "merges duplicate values" do
|
97
118
|
expect(HTTP::Request).to receive(:new) do |_, uri|
|
98
119
|
expect(uri.query).to match(/^(a=1&a=2|a=2&a=1)$/)
|
99
120
|
end
|
100
121
|
|
101
|
-
client.get(
|
122
|
+
client.get("http://example.com/?a=1", :params => {:a => 2})
|
102
123
|
end
|
103
124
|
|
104
|
-
it
|
125
|
+
it "does not modifies query part if no params were given" do
|
105
126
|
expect(HTTP::Request).to receive(:new) do |_, uri|
|
106
|
-
expect(uri.query).to eq
|
127
|
+
expect(uri.query).to eq "deadbeef"
|
107
128
|
end
|
108
129
|
|
109
|
-
client.get(
|
130
|
+
client.get("http://example.com/?deadbeef")
|
110
131
|
end
|
111
132
|
|
112
|
-
it
|
133
|
+
it "does not corrupts index-less arrays" do
|
113
134
|
expect(HTTP::Request).to receive(:new) do |_, uri|
|
114
|
-
expect(CGI.parse uri.query).to eq
|
135
|
+
expect(CGI.parse uri.query).to eq "a[]" => %w(b c), "d" => %w(e)
|
115
136
|
end
|
116
137
|
|
117
|
-
client.get(
|
138
|
+
client.get("http://example.com/?a[]=b&a[]=c", :params => {:d => "e"})
|
118
139
|
end
|
119
140
|
end
|
120
141
|
|
121
|
-
describe
|
122
|
-
it
|
142
|
+
describe "passing json" do
|
143
|
+
it "encodes given object" do
|
123
144
|
client = HTTP::Client.new
|
124
145
|
allow(client).to receive(:perform)
|
125
146
|
|
@@ -127,84 +148,91 @@ RSpec.describe HTTP::Client do
|
|
127
148
|
expect(args.last).to eq('{"foo":"bar"}')
|
128
149
|
end
|
129
150
|
|
130
|
-
client.get(
|
151
|
+
client.get("http://example.com/", :json => {:foo => :bar})
|
131
152
|
end
|
132
153
|
end
|
133
154
|
|
134
|
-
describe
|
135
|
-
context
|
136
|
-
let(:headers) { {
|
155
|
+
describe "#request" do
|
156
|
+
context "with explicitly given `Host` header" do
|
157
|
+
let(:headers) { {"Host" => "another.example.com"} }
|
137
158
|
let(:client) { described_class.new :headers => headers }
|
138
159
|
|
139
|
-
it
|
160
|
+
it "keeps `Host` header as is" do
|
140
161
|
expect(client).to receive(:perform) do |req, _|
|
141
|
-
expect(req[
|
162
|
+
expect(req["Host"]).to eq "another.example.com"
|
142
163
|
end
|
143
164
|
|
144
|
-
client.request(:get,
|
165
|
+
client.request(:get, "http://example.com/")
|
145
166
|
end
|
146
167
|
end
|
147
168
|
end
|
148
169
|
|
149
|
-
|
170
|
+
include_context "handles shared connections" do
|
171
|
+
let(:reuse_conn) { nil }
|
172
|
+
let(:server) { dummy }
|
173
|
+
let(:client) { described_class.new(:persistent => reuse_conn) }
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "SSL" do
|
177
|
+
let(:reuse_conn) { nil }
|
178
|
+
|
150
179
|
let(:client) do
|
151
180
|
described_class.new(
|
181
|
+
:persistent => reuse_conn,
|
152
182
|
:ssl_context => OpenSSL::SSL::SSLContext.new.tap do |context|
|
153
183
|
context.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
|
154
184
|
|
155
185
|
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
156
|
-
context.ca_file = File.join(certs_dir,
|
186
|
+
context.ca_file = File.join(certs_dir, "ca.crt")
|
157
187
|
context.cert = OpenSSL::X509::Certificate.new(
|
158
|
-
File.read(File.join(certs_dir,
|
188
|
+
File.read(File.join(certs_dir, "client.crt"))
|
159
189
|
)
|
160
190
|
context.key = OpenSSL::PKey::RSA.new(
|
161
|
-
File.read(File.join(certs_dir,
|
191
|
+
File.read(File.join(certs_dir, "client.key"))
|
162
192
|
)
|
163
193
|
context
|
164
194
|
end
|
165
195
|
)
|
166
196
|
end
|
167
197
|
|
168
|
-
|
198
|
+
include_context "handles shared connections" do
|
199
|
+
let(:server) { dummy_ssl }
|
200
|
+
end
|
201
|
+
|
202
|
+
it "works via SSL" do
|
169
203
|
response = client.get(dummy_ssl.endpoint)
|
170
|
-
expect(response.body.to_s).to eq(
|
204
|
+
expect(response.body.to_s).to eq("<!doctype html>")
|
171
205
|
end
|
172
206
|
|
173
|
-
context
|
174
|
-
it
|
175
|
-
expect { client.get(dummy_ssl.endpoint.gsub(
|
207
|
+
context "with a mismatch host" do
|
208
|
+
it "errors" do
|
209
|
+
expect { client.get(dummy_ssl.endpoint.gsub("127.0.0.1", "localhost")) }
|
176
210
|
.to raise_error(OpenSSL::SSL::SSLError, /does not match/)
|
177
211
|
end
|
178
212
|
end
|
179
213
|
end
|
180
214
|
|
181
|
-
describe
|
215
|
+
describe "#perform" do
|
182
216
|
let(:client) { described_class.new }
|
183
217
|
|
184
|
-
it
|
185
|
-
|
186
|
-
|
187
|
-
catch(:halt) { client.head test_endpoint }
|
188
|
-
end
|
189
|
-
|
190
|
-
it 'calls finish_response once body was fully flushed' do
|
191
|
-
expect(client).to receive(:finish_response).twice.and_call_original
|
192
|
-
client.get(test_endpoint).to_s
|
218
|
+
it "calls finish_response once body was fully flushed" do
|
219
|
+
expect_any_instance_of(HTTP::Connection).to receive(:finish_response).and_call_original
|
220
|
+
client.get(dummy.endpoint).to_s
|
193
221
|
end
|
194
222
|
|
195
|
-
context
|
196
|
-
it
|
197
|
-
|
198
|
-
client.head(
|
223
|
+
context "with HEAD request" do
|
224
|
+
it "does not iterates through body" do
|
225
|
+
expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
|
226
|
+
client.head(dummy.endpoint)
|
199
227
|
end
|
200
228
|
|
201
|
-
it
|
202
|
-
|
203
|
-
client.head(
|
229
|
+
it "finishes response after headers were received" do
|
230
|
+
expect_any_instance_of(HTTP::Connection).to receive(:finish_response).and_call_original
|
231
|
+
client.head(dummy.endpoint)
|
204
232
|
end
|
205
233
|
end
|
206
234
|
|
207
|
-
context
|
235
|
+
context "when server closes connection unexpectedly" do
|
208
236
|
before do
|
209
237
|
socket_spy = double
|
210
238
|
|
@@ -216,7 +244,7 @@ RSpec.describe HTTP::Client do
|
|
216
244
|
allow(TCPSocket).to receive(:open) { socket_spy }
|
217
245
|
end
|
218
246
|
|
219
|
-
context
|
247
|
+
context "during headers reading" do
|
220
248
|
let :chunks do
|
221
249
|
[
|
222
250
|
proc { "HTTP/1.1 200 OK\r\n" },
|
@@ -225,28 +253,28 @@ RSpec.describe HTTP::Client do
|
|
225
253
|
]
|
226
254
|
end
|
227
255
|
|
228
|
-
it
|
229
|
-
expect { client.get
|
256
|
+
it "raises IOError" do
|
257
|
+
expect { client.get dummy.endpoint }.to raise_error IOError
|
230
258
|
end
|
231
259
|
end
|
232
260
|
|
233
|
-
context
|
261
|
+
context "after headers were flushed" do
|
234
262
|
let :chunks do
|
235
263
|
[
|
236
264
|
proc { "HTTP/1.1 200 OK\r\n" },
|
237
265
|
proc { "Content-Type: text/html\r\n\r\n" },
|
238
|
-
proc {
|
266
|
+
proc { "unexpected end of f" },
|
239
267
|
proc { fail EOFError }
|
240
268
|
]
|
241
269
|
end
|
242
270
|
|
243
|
-
it
|
244
|
-
res = client.get(
|
245
|
-
expect(res).to eq
|
271
|
+
it "reads partially arrived body" do
|
272
|
+
res = client.get(dummy.endpoint).to_s
|
273
|
+
expect(res).to eq "unexpected end of f"
|
246
274
|
end
|
247
275
|
end
|
248
276
|
|
249
|
-
context
|
277
|
+
context "when body and headers were flushed in one chunk" do
|
250
278
|
let :chunks do
|
251
279
|
[
|
252
280
|
proc { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nunexpected end of f" },
|
@@ -254,19 +282,19 @@ RSpec.describe HTTP::Client do
|
|
254
282
|
]
|
255
283
|
end
|
256
284
|
|
257
|
-
it
|
258
|
-
res = client.get(
|
259
|
-
expect(res).to eq
|
285
|
+
it "reads partially arrived body" do
|
286
|
+
res = client.get(dummy.endpoint).to_s
|
287
|
+
expect(res).to eq "unexpected end of f"
|
260
288
|
end
|
261
289
|
end
|
262
290
|
end
|
263
291
|
|
264
|
-
context
|
292
|
+
context "when server fully flushes response in one chunk" do
|
265
293
|
before do
|
266
294
|
socket_spy = double
|
267
295
|
|
268
296
|
chunks = [
|
269
|
-
<<-RESPONSE.gsub(/^\s*\| */,
|
297
|
+
<<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
|
270
298
|
| HTTP/1.1 200 OK
|
271
299
|
| Content-Type: text/html
|
272
300
|
| Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-11-22)
|
@@ -286,9 +314,9 @@ RSpec.describe HTTP::Client do
|
|
286
314
|
allow(TCPSocket).to receive(:open) { socket_spy }
|
287
315
|
end
|
288
316
|
|
289
|
-
it
|
290
|
-
body = client.get(
|
291
|
-
expect(body).to eq
|
317
|
+
it "properly reads body" do
|
318
|
+
body = client.get(dummy.endpoint).to_s
|
319
|
+
expect(body).to eq "<!doctype html>"
|
292
320
|
end
|
293
321
|
end
|
294
322
|
end
|