File: excon.rb

package info (click to toggle)
ruby-vcr 6.0.0%2Breally5.0.0-5
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,320 kB
  • sloc: ruby: 8,456; sh: 177; makefile: 7
file content (221 lines) | stat: -rw-r--r-- 6,448 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
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 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