Permalink
Browse files

Add after_http_request hook.

For issue #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.
  • Loading branch information...
myronmarston committed Nov 18, 2011
1 parent a8110df commit 34d6536a45ecea9ec5d14f7bf67796e6e2561d0e
View
@@ -10,6 +10,7 @@ class Configuration
define_hook :before_playback
define_hook :after_library_hooks_loaded
define_hook :before_http_request
+ define_hook :after_http_request
def initialize
@allow_http_connections_when_no_cassette = nil
@@ -10,12 +10,20 @@ module Excon
class RequestHandler < ::VCR::RequestHandler
attr_reader :params
def initialize(params)
+ @vcr_response = nil
@params = params
end
+ def handle
+ super
+ ensure
+ invoke_after_request_hook(@vcr_response)
+ end
+
private
def on_stubbed_request
+ @vcr_response = stubbed_response
{
:body => stubbed_response.body,
:headers => normalized_headers(stubbed_response.headers || {}),
@@ -38,16 +46,15 @@ def response_from_excon_error(error)
end
def perform_real_request
- connection = ::Excon.new(uri)
-
- response = begin
- connection.request(params.merge(:mock => false))
- rescue ::Excon::Errors::Error => e
- yield response_from_excon_error(e) if block_given?
- raise e
+ begin
+ response = ::Excon.new(uri).request(params.merge(:mock => false))
+ rescue ::Excon::Errors::Error => excon_error
+ response = response_from_excon_error(excon_error)
end
+ @vcr_response = vcr_response_from(response)
yield response if block_given?
+ raise excon_error if excon_error
response.attributes
end
@@ -87,7 +94,7 @@ def query
def http_interaction_for(response)
VCR::HTTPInteraction.new \
vcr_request,
- vcr_response(response)
+ vcr_response_from(response)
end
def vcr_request
@@ -103,7 +110,7 @@ def vcr_request
end
end
- def vcr_response(response)
+ def vcr_response_from(response)
VCR::Response.new \
VCR::ResponseStatus.new(response.status, nil),
response.headers,
@@ -14,44 +14,60 @@ class RequestHandler < ::VCR::RequestHandler
def initialize(net_http, request, request_body = nil, &response_block)
@net_http, @request, @request_body, @response_block =
net_http, request, request_body, response_block
+ @vcr_response, @recursing = nil, false
+ end
+
+ def handle
+ super
+ ensure
+ invoke_after_request_hook(@vcr_response) unless @recursing
end
private
+
def on_recordable_request
- perform_and_record_request
+ perform_request(net_http.started?, :record_interaction)
end
def on_stubbed_request
- perform_stubbed_request
+ with_exclusive_fakeweb_stub(stubbed_response) do
+ # force it to be considered started since it doesn't
+ # recurse in this case like the others.
+ perform_request(:started)
+ end
end
def on_ignored_request
- perform_request(&response_block)
+ perform_request(net_http.started?)
+ end
+
+ # overriden to prevent it from invoking the after_http_request hook,
+ # since we invoke the hook in an ensure block above.
+ def on_connection_not_allowed
+ raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request)
end
- def perform_and_record_request
+ def perform_request(started, record_interaction = false)
# Net::HTTP calls #request recursively in certain circumstances.
# We only want to record the request when the request is started, as
# that is the final time through #request.
- return perform_request(&response_block) unless net_http.started?
+ unless started
+ @recursing = true
+ return net_http.request_without_vcr(request, request_body, &response_block)
+ end
+
+ net_http.request_without_vcr(request, request_body) do |response|
+ @vcr_response = vcr_response_from(response)
+
+ if record_interaction
+ VCR.record_http_interaction VCR::HTTPInteraction.new(vcr_request, @vcr_response)
+ end
- perform_request do |response|
- VCR.record_http_interaction VCR::HTTPInteraction.new(vcr_request, vcr_response_from(response))
response.extend VCR::Net::HTTPResponse # "unwind" the response
response_block.call(response) if response_block
end
end
- def perform_stubbed_request
- with_exclusive_fakeweb_stub(stubbed_response) do
- perform_request(&response_block)
- end
- end
-
- def perform_request(&block)
- net_http.request_without_vcr(request, request_body, &block)
- end
-
def uri
@uri ||= ::FakeWeb::Utility.request_uri_as_string(net_http, request)
end
@@ -35,6 +35,11 @@ def initialize(request)
private
+ def on_connection_not_allowed
+ invoke_after_request_hook(nil)
+ super
+ end
+
def vcr_request
@vcr_request ||= vcr_request_from(request)
end
@@ -59,9 +64,15 @@ def stubbed_response_headers
extend Helpers
::Typhoeus::Hydra.after_request_before_on_complete do |request|
- unless VCR.library_hooks.disabled?(:typhoeus) || request.response.mock?
- http_interaction = VCR::HTTPInteraction.new(vcr_request_from(request), vcr_response_from(request.response))
- VCR.record_http_interaction(http_interaction)
+ unless VCR.library_hooks.disabled?(:typhoeus)
+ vcr_request, vcr_response = vcr_request_from(request), vcr_response_from(request.response)
+
+ unless request.response.mock?
+ http_interaction = VCR::HTTPInteraction.new(vcr_request, vcr_response)
+ VCR.record_http_interaction(http_interaction)
+ end
+
+ VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request, vcr_response)
end
end
@@ -39,6 +39,11 @@ def vcr_request
@vcr_request ||= vcr_request_from(request)
end
+ def on_connection_not_allowed
+ invoke_after_request_hook(nil)
+ super
+ end
+
def on_stubbed_request
{
:body => stubbed_response.body,
@@ -61,6 +66,12 @@ def on_stubbed_request
VCR.record_http_interaction(http_interaction)
end
end
+
+ ::WebMock.after_request do |request, response|
+ unless VCR.library_hooks.disabled?(:webmock)
+ VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request_from(request), vcr_response_from(response))
+ end
+ end
end
end
end
@@ -28,8 +28,18 @@ def initialize(app, env)
@app, @env = app, env
end
+ def handle
+ super
+ ensure
+ invoke_after_request_hook(response_for(env)) unless running_in_parallel?
+ end
+
private
+ def running_in_parallel?
+ !!env[:parallel_manager]
+ end
+
def vcr_request
@vcr_request ||= VCR::Request.new \
env[:method],
@@ -40,6 +50,7 @@ def vcr_request
def response_for(env)
response = env[:response]
+ return nil unless response
VCR::Response.new(
VCR::ResponseStatus.new(response.status, nil),
@@ -66,6 +77,7 @@ def on_stubbed_request
def on_recordable_request
app.call(env).on_complete do |env|
VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, response_for(env)))
+ invoke_after_request_hook(response_for(env)) if running_in_parallel?
end
end
end
@@ -15,6 +15,11 @@ def invoke_before_request_hook
VCR.configuration.invoke_hook(:before_http_request, tag = nil, vcr_request)
end
+ def invoke_after_request_hook(vcr_response)
+ return if disabled?
+ VCR.configuration.invoke_hook(:after_http_request, tag = nil, vcr_request, vcr_response)
+ end
+
def should_ignore?
disabled? || VCR.request_ignorer.ignore?(vcr_request)
end
@@ -0,0 +1,54 @@
+shared_examples_for "after_http_request hook" do
+ let(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" }
+
+ def make_request(disabled = false)
+ make_http_request(:get, request_url)
+ end
+
+ def assert_expected_response(response)
+ response.status.code.should eq(200)
+ response.body.should eq('FOO!')
+ end
+
+ it 'invokes the hook only once per request' do
+ call_count = 0
+ VCR.configure do |c|
+ c.after_http_request { |r| call_count += 1 }
+ end
+ make_request
+ call_count.should eq(1)
+ end
+
+ it 'yields the request to the hook' do
+ request = nil
+ VCR.configure do |c|
+ c.after_http_request { |r| request = r }
+ end
+ make_request
+ request.method.should be(:get)
+ request.uri.should eq(request_url)
+ end
+
+ it 'yields the response to the hook if a second block arg is given' do
+ response = nil
+ VCR.configure do |c|
+ c.after_http_request { |req, res| response = res }
+ end
+ make_request
+ assert_expected_response(response)
+ end
+
+ it 'does not run the hook if the library hook is disabled' do
+ VCR.library_hooks.should respond_to(:disabled?)
+ VCR.library_hooks.stub(:disabled? => true)
+
+ hook_called = false
+ VCR.configure do |c|
+ c.after_http_request { |r| hook_called = true }
+ end
+
+ make_request(:disabled)
+ hook_called.should be_false
+ end
+end
+
@@ -157,6 +157,67 @@ def self.test_playback(description, url)
end
end
+ context 'when there is an after_http_request hook' do
+ context 'when the request is ignored' do
+ before(:each) do
+ VCR.configuration.ignore_request { |r| true }
+ end
+
+ it_behaves_like "after_http_request hook"
+ end
+
+ context 'when the request is recorded' do
+ let!(:inserted_cassette) { VCR.insert_cassette('new_cassette') }
+
+ it_behaves_like "after_http_request hook" do
+ it 'can be used to eject a cassette after the request is recorded' do
+ VCR.configuration.after_http_request do |request|
+ VCR.eject_cassette
+ end
+
+ VCR.should_receive(:record_http_interaction) do |interaction|
+ VCR.current_cassette.should be(inserted_cassette)
+ end
+
+ make_http_request(:get, request_url)
+ VCR.current_cassette.should be_nil
+ end
+ end
+ end
+
+ context 'when the request is played back' do
+ it_behaves_like "after_http_request hook" do
+ let(:request) { VCR::Request.new(:get, request_url) }
+ let(:response_body) { "FOO!" }
+ let(:response) { VCR::Response.new(status, nil, response_body, '1.1') }
+ let(:status) { VCR::ResponseStatus.new(200, 'OK') }
+ let(:interaction) { VCR::HTTPInteraction.new(request, response) }
+
+ before(:each) do
+ stub_requests([interaction], [:method, :uri])
+ end
+ end
+ end
+
+ context 'when the request is not allowed' do
+ it_behaves_like "after_http_request hook" do
+ undef assert_expected_response
+ def assert_expected_response(response)
+ response.should be_nil
+ end
+
+ undef make_request
+ def make_request(disabled = false)
+ if disabled
+ make_http_request(:get, request_url)
+ else
+ expect { make_http_request(:get, request_url) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR)
+ end
+ end
+ end
+ end
+ end
+
describe '.stub_requests using specific match_attributes' do
before(:each) { VCR.stub(:real_http_connections_allowed? => false) }
let(:interactions) { interactions_from('match_requests_on.yml') }
Oops, something went wrong.

0 comments on commit 34d6536

Please sign in to comment.