Skip to content

Commit 34d6536

Browse files
committed
Add after_http_request hook.
For issue vcr#91. Note that when using Typhoeus or WebMock, it's not guaranteed that the after_request hook will fire for every request; if an error (such as a socket error) occurs before the request completes then the Typhoeus/WebMock after_request hook will not fire, which means VCR will not be able to fire its after_http_request hook in turn. There's not much I can do about this, unfortunately. For Excon, Faraday and FakeWeb, I use an ensure block to guarantee that the hook will be invoked. This is possible because VCR wraps the entire request for these library hooks.
1 parent a8110df commit 34d6536

File tree

12 files changed

+270
-30
lines changed

12 files changed

+270
-30
lines changed

lib/vcr/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Configuration
1010
define_hook :before_playback
1111
define_hook :after_library_hooks_loaded
1212
define_hook :before_http_request
13+
define_hook :after_http_request
1314

1415
def initialize
1516
@allow_http_connections_when_no_cassette = nil

lib/vcr/library_hooks/excon.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ module Excon
1010
class RequestHandler < ::VCR::RequestHandler
1111
attr_reader :params
1212
def initialize(params)
13+
@vcr_response = nil
1314
@params = params
1415
end
1516

17+
def handle
18+
super
19+
ensure
20+
invoke_after_request_hook(@vcr_response)
21+
end
22+
1623
private
1724

1825
def on_stubbed_request
26+
@vcr_response = stubbed_response
1927
{
2028
:body => stubbed_response.body,
2129
:headers => normalized_headers(stubbed_response.headers || {}),
@@ -38,16 +46,15 @@ def response_from_excon_error(error)
3846
end
3947

4048
def perform_real_request
41-
connection = ::Excon.new(uri)
42-
43-
response = begin
44-
connection.request(params.merge(:mock => false))
45-
rescue ::Excon::Errors::Error => e
46-
yield response_from_excon_error(e) if block_given?
47-
raise e
49+
begin
50+
response = ::Excon.new(uri).request(params.merge(:mock => false))
51+
rescue ::Excon::Errors::Error => excon_error
52+
response = response_from_excon_error(excon_error)
4853
end
4954

55+
@vcr_response = vcr_response_from(response)
5056
yield response if block_given?
57+
raise excon_error if excon_error
5158

5259
response.attributes
5360
end
@@ -87,7 +94,7 @@ def query
8794
def http_interaction_for(response)
8895
VCR::HTTPInteraction.new \
8996
vcr_request,
90-
vcr_response(response)
97+
vcr_response_from(response)
9198
end
9299

93100
def vcr_request
@@ -103,7 +110,7 @@ def vcr_request
103110
end
104111
end
105112

106-
def vcr_response(response)
113+
def vcr_response_from(response)
107114
VCR::Response.new \
108115
VCR::ResponseStatus.new(response.status, nil),
109116
response.headers,

lib/vcr/library_hooks/fakeweb.rb

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,60 @@ class RequestHandler < ::VCR::RequestHandler
1414
def initialize(net_http, request, request_body = nil, &response_block)
1515
@net_http, @request, @request_body, @response_block =
1616
net_http, request, request_body, response_block
17+
@vcr_response, @recursing = nil, false
18+
end
19+
20+
def handle
21+
super
22+
ensure
23+
invoke_after_request_hook(@vcr_response) unless @recursing
1724
end
1825

1926
private
27+
2028
def on_recordable_request
21-
perform_and_record_request
29+
perform_request(net_http.started?, :record_interaction)
2230
end
2331

2432
def on_stubbed_request
25-
perform_stubbed_request
33+
with_exclusive_fakeweb_stub(stubbed_response) do
34+
# force it to be considered started since it doesn't
35+
# recurse in this case like the others.
36+
perform_request(:started)
37+
end
2638
end
2739

2840
def on_ignored_request
29-
perform_request(&response_block)
41+
perform_request(net_http.started?)
42+
end
43+
44+
# overriden to prevent it from invoking the after_http_request hook,
45+
# since we invoke the hook in an ensure block above.
46+
def on_connection_not_allowed
47+
raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request)
3048
end
3149

32-
def perform_and_record_request
50+
def perform_request(started, record_interaction = false)
3351
# Net::HTTP calls #request recursively in certain circumstances.
3452
# We only want to record the request when the request is started, as
3553
# that is the final time through #request.
36-
return perform_request(&response_block) unless net_http.started?
54+
unless started
55+
@recursing = true
56+
return net_http.request_without_vcr(request, request_body, &response_block)
57+
end
58+
59+
net_http.request_without_vcr(request, request_body) do |response|
60+
@vcr_response = vcr_response_from(response)
61+
62+
if record_interaction
63+
VCR.record_http_interaction VCR::HTTPInteraction.new(vcr_request, @vcr_response)
64+
end
3765

38-
perform_request do |response|
39-
VCR.record_http_interaction VCR::HTTPInteraction.new(vcr_request, vcr_response_from(response))
4066
response.extend VCR::Net::HTTPResponse # "unwind" the response
4167
response_block.call(response) if response_block
4268
end
4369
end
4470

45-
def perform_stubbed_request
46-
with_exclusive_fakeweb_stub(stubbed_response) do
47-
perform_request(&response_block)
48-
end
49-
end
50-
51-
def perform_request(&block)
52-
net_http.request_without_vcr(request, request_body, &block)
53-
end
54-
5571
def uri
5672
@uri ||= ::FakeWeb::Utility.request_uri_as_string(net_http, request)
5773
end

lib/vcr/library_hooks/typhoeus.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ def initialize(request)
3535

3636
private
3737

38+
def on_connection_not_allowed
39+
invoke_after_request_hook(nil)
40+
super
41+
end
42+
3843
def vcr_request
3944
@vcr_request ||= vcr_request_from(request)
4045
end
@@ -59,9 +64,15 @@ def stubbed_response_headers
5964

6065
extend Helpers
6166
::Typhoeus::Hydra.after_request_before_on_complete do |request|
62-
unless VCR.library_hooks.disabled?(:typhoeus) || request.response.mock?
63-
http_interaction = VCR::HTTPInteraction.new(vcr_request_from(request), vcr_response_from(request.response))
64-
VCR.record_http_interaction(http_interaction)
67+
unless VCR.library_hooks.disabled?(:typhoeus)
68+
vcr_request, vcr_response = vcr_request_from(request), vcr_response_from(request.response)
69+
70+
unless request.response.mock?
71+
http_interaction = VCR::HTTPInteraction.new(vcr_request, vcr_response)
72+
VCR.record_http_interaction(http_interaction)
73+
end
74+
75+
VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request, vcr_response)
6576
end
6677
end
6778

lib/vcr/library_hooks/webmock.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def vcr_request
3939
@vcr_request ||= vcr_request_from(request)
4040
end
4141

42+
def on_connection_not_allowed
43+
invoke_after_request_hook(nil)
44+
super
45+
end
46+
4247
def on_stubbed_request
4348
{
4449
:body => stubbed_response.body,
@@ -61,6 +66,12 @@ def on_stubbed_request
6166
VCR.record_http_interaction(http_interaction)
6267
end
6368
end
69+
70+
::WebMock.after_request do |request, response|
71+
unless VCR.library_hooks.disabled?(:webmock)
72+
VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request_from(request), vcr_response_from(response))
73+
end
74+
end
6475
end
6576
end
6677
end

lib/vcr/middleware/faraday.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@ def initialize(app, env)
2828
@app, @env = app, env
2929
end
3030

31+
def handle
32+
super
33+
ensure
34+
invoke_after_request_hook(response_for(env)) unless running_in_parallel?
35+
end
36+
3137
private
3238

39+
def running_in_parallel?
40+
!!env[:parallel_manager]
41+
end
42+
3343
def vcr_request
3444
@vcr_request ||= VCR::Request.new \
3545
env[:method],
@@ -40,6 +50,7 @@ def vcr_request
4050

4151
def response_for(env)
4252
response = env[:response]
53+
return nil unless response
4354

4455
VCR::Response.new(
4556
VCR::ResponseStatus.new(response.status, nil),
@@ -66,6 +77,7 @@ def on_stubbed_request
6677
def on_recordable_request
6778
app.call(env).on_complete do |env|
6879
VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, response_for(env)))
80+
invoke_after_request_hook(response_for(env)) if running_in_parallel?
6981
end
7082
end
7183
end

lib/vcr/request_handler.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ def invoke_before_request_hook
1515
VCR.configuration.invoke_hook(:before_http_request, tag = nil, vcr_request)
1616
end
1717

18+
def invoke_after_request_hook(vcr_response)
19+
return if disabled?
20+
VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request, vcr_response)
21+
end
22+
1823
def should_ignore?
1924
disabled? || VCR.request_ignorer.ignore?(vcr_request)
2025
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
shared_examples_for "after_http_request hook" do
2+
let(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" }
3+
4+
def make_request(disabled = false)
5+
make_http_request(:get, request_url)
6+
end
7+
8+
def assert_expected_response(response)
9+
response.status.code.should eq(200)
10+
response.body.should eq('FOO!')
11+
end
12+
13+
it 'invokes the hook only once per request' do
14+
call_count = 0
15+
VCR.configure do |c|
16+
c.after_http_request { |r| call_count += 1 }
17+
end
18+
make_request
19+
call_count.should eq(1)
20+
end
21+
22+
it 'yields the request to the hook' do
23+
request = nil
24+
VCR.configure do |c|
25+
c.after_http_request { |r| request = r }
26+
end
27+
make_request
28+
request.method.should be(:get)
29+
request.uri.should eq(request_url)
30+
end
31+
32+
it 'yields the response to the hook if a second block arg is given' do
33+
response = nil
34+
VCR.configure do |c|
35+
c.after_http_request { |req, res| response = res }
36+
end
37+
make_request
38+
assert_expected_response(response)
39+
end
40+
41+
it 'does not run the hook if the library hook is disabled' do
42+
VCR.library_hooks.should respond_to(:disabled?)
43+
VCR.library_hooks.stub(:disabled? => true)
44+
45+
hook_called = false
46+
VCR.configure do |c|
47+
c.after_http_request { |r| hook_called = true }
48+
end
49+
50+
make_request(:disabled)
51+
hook_called.should be_false
52+
end
53+
end
54+

spec/support/shared_example_groups/hook_into_http_library.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,67 @@ def self.test_playback(description, url)
157157
end
158158
end
159159

160+
context 'when there is an after_http_request hook' do
161+
context 'when the request is ignored' do
162+
before(:each) do
163+
VCR.configuration.ignore_request { |r| true }
164+
end
165+
166+
it_behaves_like "after_http_request hook"
167+
end
168+
169+
context 'when the request is recorded' do
170+
let!(:inserted_cassette) { VCR.insert_cassette('new_cassette') }
171+
172+
it_behaves_like "after_http_request hook" do
173+
it 'can be used to eject a cassette after the request is recorded' do
174+
VCR.configuration.after_http_request do |request|
175+
VCR.eject_cassette
176+
end
177+
178+
VCR.should_receive(:record_http_interaction) do |interaction|
179+
VCR.current_cassette.should be(inserted_cassette)
180+
end
181+
182+
make_http_request(:get, request_url)
183+
VCR.current_cassette.should be_nil
184+
end
185+
end
186+
end
187+
188+
context 'when the request is played back' do
189+
it_behaves_like "after_http_request hook" do
190+
let(:request) { VCR::Request.new(:get, request_url) }
191+
let(:response_body) { "FOO!" }
192+
let(:response) { VCR::Response.new(status, nil, response_body, '1.1') }
193+
let(:status) { VCR::ResponseStatus.new(200, 'OK') }
194+
let(:interaction) { VCR::HTTPInteraction.new(request, response) }
195+
196+
before(:each) do
197+
stub_requests([interaction], [:method, :uri])
198+
end
199+
end
200+
end
201+
202+
context 'when the request is not allowed' do
203+
it_behaves_like "after_http_request hook" do
204+
undef assert_expected_response
205+
def assert_expected_response(response)
206+
response.should be_nil
207+
end
208+
209+
undef make_request
210+
def make_request(disabled = false)
211+
if disabled
212+
make_http_request(:get, request_url)
213+
else
214+
expect { make_http_request(:get, request_url) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR)
215+
end
216+
end
217+
end
218+
end
219+
end
220+
160221
describe '.stub_requests using specific match_attributes' do
161222
before(:each) { VCR.stub(:real_http_connections_allowed? => false) }
162223
let(:interactions) { interactions_from('match_requests_on.yml') }

0 commit comments

Comments
 (0)