lib/vcr/middleware/excon.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'excon'
require 'vcr/request_handler'
require 'vcr/util/version_checker'

VCR::VersionChecker.new('Excon', Excon::VERSION, '0.25.2').check_version!

module VCR
  # Contains middlewares for use with different libraries.
  module Middleware
    # Contains Excon middlewares.
    module Excon
      # One part of the Excon middleware that uses VCR to record
      # and replay HTTP requests made through Excon.
      #
      # @private
      class Request < ::Excon::Middleware::Base
        # @private
        def request_call(params)
          params[:vcr_request_handler] = request_handler = RequestHandler.new
          request_handler.before_request(params)

          super
        end
      end

      # One part of the Excon middleware that uses VCR to record
      # and replay HTTP requests made through Excon.
      #
      # @private
      class Response < ::Excon::Middleware::Base
        # @private
        def response_call(params)
          complete_request(params)
          super
        end

        def error_call(params)
          complete_request(params)
          super
        end

      private

        def complete_request(params)
          if handler = params.delete(:vcr_request_handler)
            handler.after_request(params[:response])
          end
        end
      end

      # Handles a single Excon request.
      #
      # @private
      class RequestHandler < ::VCR::RequestHandler
        def initialize
          @request_params       = nil
          @response_body_reader = nil
          @should_record        = false
        end

        # Performs before_request processing based on the provided
        # request_params.
        #
        # @private
        def before_request(request_params)
          @request_params       = request_params
          @response_body_reader = create_response_body_reader
          handle
        end

        # Performs after_request processing based on the provided response.
        #
        # @private
        def after_request(response)
          vcr_response = vcr_response_for(response)

          if vcr_response && should_record?
            VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, vcr_response))
          end

          invoke_after_request_hook(vcr_response)
        end

        def ensure_response_body_can_be_read_for_error_case
          # Excon does not invoke the `:response_block` when an error
          # has occurred, so we need to be sure to use the non-streaming
          # body reader.
          @response_body_reader = NonStreamingResponseBodyReader
        end

        attr_reader :request_params, :response_body_reader

      private

        def externally_stubbed?
          !!::Excon.stub_for(request_params)
        end

        def should_record?
          @should_record
        end

        def on_stubbed_by_vcr_request
          request_params[:response] = {
            :body     => stubbed_response.body.dup, # Excon mutates the body, so we must dup it :-(
            :headers  => normalized_headers(stubbed_response.headers || {}),
            :status   => stubbed_response.status.code
          }
        end

        def on_recordable_request
          @should_record = true
        end

        def create_response_body_reader
          block = request_params[:response_block]
          return NonStreamingResponseBodyReader unless block

          StreamingResponseBodyReader.new(block).tap do |response_block_wrapper|
            request_params[:response_block] = response_block_wrapper
          end
        end

        def vcr_request
          @vcr_request ||= begin
            headers = request_params[:headers].dup
            headers.delete("Host")

            VCR::Request.new \
              request_params[:method],
              uri,
              request_params[:body],
              headers
          end
        end

        def vcr_response_for(response)
          return nil if response.nil?

          VCR::Response.new(
            VCR::ResponseStatus.new(response.fetch(:status), nil),
            response.fetch(:headers),
            response_body_reader.read_body_from(response),
            nil
          )
        end

        def normalized_headers(headers)
          normalized = {}
          headers.each do |k, v|
            v = v.join(', ') if v.respond_to?(:join)
            normalized[k] = v
          end
          normalized
        end

        if defined?(::Excon::Utils) && ::Excon::Utils.respond_to?(:request_uri)
          def uri
            @uri ||= "#{::Excon::Utils.request_uri(request_params)}"
          end
        else
          require 'vcr/middleware/excon/legacy_methods'
          include LegacyMethods

          def uri
            @uri ||= "#{request_params[:scheme]}://#{request_params[:host]}:#{request_params[:port]}#{request_params[:path]}#{query}"
          end
        end
      end

      # Wraps an Excon streaming `:response_block`, so that we can
      # accumulate the response as it streams back from the real HTTP
      # server in order to record it.
      #
      # @private
      class StreamingResponseBodyReader
        def initialize(response_block)
          @response_block = response_block
          @chunks = []
        end

        # @private
        def call(chunk, remaining_bytes, total_bytes)
          @chunks << chunk
          @response_block.call(chunk, remaining_bytes, total_bytes)
        end

        # Provides a duck-typed interface that matches that of
        # `NonStreamingResponseBodyReader`. The request handler
        # will use this to get the response body.
        #
        # @private
        def read_body_from(response_params)
          if @chunks.none?
            # Not sure why, but sometimes the body comes through the params
            # instead of via the streaming block even when the block was
            # configured.
            response_params[:body]
          else
            @chunks.join('')
          end
        end
      end

      # Reads the body when no streaming is done.
      #
      # @private
      class NonStreamingResponseBodyReader
        # Provides a duck-typed interface that matches that of
        # `StreamingResponseBodyReader`. The request handler
        # will use this to get the response body.
        #
        # @private
        def self.read_body_from(response_params)
          response_params.fetch(:body)
        end
      end
    end
  end
end