File: async_http_client_adapter.rb

package info (click to toggle)
ruby-webmock 3.25.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,172 kB
  • sloc: ruby: 12,829; makefile: 6
file content (228 lines) | stat: -rw-r--r-- 6,868 bytes parent folder | download
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
222
223
224
225
226
227
228
# frozen_string_literal: true

begin
  require 'async'
  require 'async/http'
rescue LoadError
  # async-http not found
end

if defined?(Async::HTTP)
  module WebMock
    module HttpLibAdapters
      class AsyncHttpClientAdapter < HttpLibAdapter
        adapter_for :async_http_client

        OriginalAsyncHttpClient = Async::HTTP::Client unless const_defined?(:OriginalAsyncHttpClient)

        class << self
          def enable!
            Async::HTTP.send(:remove_const, :Client)
            Async::HTTP.send(:const_set, :Client, Async::HTTP::WebMockClientWrapper)
          end

          def disable!
            Async::HTTP.send(:remove_const, :Client)
            Async::HTTP.send(:const_set, :Client, OriginalAsyncHttpClient)
          end
        end
      end
    end
  end

  module Async
    module HTTP
      class WebMockClientWrapper < Client
        def initialize(
          endpoint,
          protocol: endpoint.protocol,
          scheme: endpoint.scheme,
          authority: endpoint.authority,
          **options
        )
          webmock_endpoint = WebMockEndpoint.new(scheme, authority, protocol)

          @network_client = WebMockClient.new(endpoint, **options)
          @webmock_client = WebMockClient.new(webmock_endpoint, **options)

          @endpoint = endpoint
          @scheme = scheme
          @authority = authority
        end

        def call(request)
          request.scheme ||= self.scheme
          request.authority ||= self.authority

          request_signature = build_request_signature(request)
          WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
          webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
          net_connect_allowed = WebMock.net_connect_allowed?(request_signature.uri)
          real_request = false

          if webmock_response
            webmock_response.raise_error_if_any
            raise Async::TimeoutError, 'WebMock timeout error' if webmock_response.should_timeout
            WebMockApplication.add_webmock_response(request, webmock_response)
            response = @webmock_client.call(request)
          elsif net_connect_allowed
            response = @network_client.call(request)
            real_request = true
          else
            raise WebMock::NetConnectNotAllowedError.new(request_signature) unless webmock_response
          end

          if WebMock::CallbackRegistry.any_callbacks?
            webmock_response ||= build_webmock_response(response)
            WebMock::CallbackRegistry.invoke_callbacks(
              {
                lib: :async_http_client,
                real_request: real_request
              },
              request_signature,
              webmock_response
            )
          end

          response
        end

        def close
          @network_client.close
          @webmock_client.close
        end

        private

        def build_request_signature(request)
          body = request.read
          request.body = ::Protocol::HTTP::Body::Buffered.wrap(body)
          WebMock::RequestSignature.new(
            request.method.downcase.to_sym,
            "#{request.scheme}://#{request.authority}#{request.path}",
            headers: request.headers.to_h,
            body: body
          )
        end

        def build_webmock_response(response)
          body = response.read
          response.body = ::Protocol::HTTP::Body::Buffered.wrap(body)

          webmock_response = WebMock::Response.new
          webmock_response.status = [
            response.status,
            ::Protocol::HTTP1::Reason::DESCRIPTIONS[response.status]
          ]
          webmock_response.headers = build_webmock_response_headers(response)
          webmock_response.body = body
          webmock_response
        end

        def build_webmock_response_headers(response)
          response.headers.each.each_with_object({}) do |(k, v), o|
            o[k] ||= []
            o[k] << v
          end
        end
      end

      class WebMockClient < Client
      end

      class WebMockEndpoint
        def initialize(scheme, authority, protocol)
          @scheme = scheme
          @authority = authority
          @protocol = protocol
        end

        attr :scheme, :authority, :protocol

        def connect
          server_socket, client_socket = create_connected_sockets
          Async(transient: true) do
            accept_socket(server_socket)
          end
          client_socket
        end

        def inspect
          "\#<#{self.class}> #{scheme}://#{authority} protocol=#{protocol}"
        end

        private

        def socket_class
          defined?(Async::IO::Socket) ? Async::IO::Socket : Socket
        end

        def create_connected_sockets
          pair = begin
            socket_class.pair(Socket::AF_UNIX, Socket::SOCK_STREAM)
          rescue Errno::EAFNOSUPPORT
            socket_class.pair(Socket::AF_INET, Socket::SOCK_STREAM)
          end
          pair.tap do |sockets|
            sockets.each do |socket|
              socket.instance_variable_set :@alpn_protocol, nil
              socket.instance_eval do
                def alpn_protocol
                  nil # means HTTP11 will be used for HTTPS
                end
              end
            end
          end
        end

        def accept_socket(socket)
          server = Async::HTTP::Server.new(WebMockApplication, self)
          server.accept(socket, socket.remote_address)
        end
      end

      module WebMockApplication
        WEBMOCK_REQUEST_ID_HEADER = 'x-webmock-request-id'.freeze

        class << self
          def call(request)
            request.read
            webmock_response = get_webmock_response(request)
            build_response(webmock_response)
          end

          def add_webmock_response(request, webmock_response)
            webmock_request_id = request.object_id.to_s
            request.headers.add(WEBMOCK_REQUEST_ID_HEADER, webmock_request_id)
            webmock_responses[webmock_request_id] = webmock_response
          end

          def get_webmock_response(request)
            webmock_request_id = request.headers[WEBMOCK_REQUEST_ID_HEADER][0]
            webmock_responses.fetch(webmock_request_id)
          end

          private

          def webmock_responses
            @webmock_responses ||= {}
          end

          def build_response(webmock_response)
            headers = (webmock_response.headers || {}).each_with_object([]) do |(k, value), o|
              Array(value).each do |v|
                o.push [k, v]
              end
            end

            ::Protocol::HTTP::Response[
              webmock_response.status[0],
              headers,
              webmock_response.body
            ]
          end
        end
      end
    end
  end
end