3
attr_reader :connection, :proxy
5
# Initializes a new Connection instance
6
# @param [String] url The destination URL
7
# @param [Hash<Symbol, >] params One or more optional params
8
# @option params [String] :body Default text to be sent over a socket. Only used if :body absent in Connection#request params
9
# @option params [Hash<Symbol, String>] :headers The default headers to supply in a request. Only used if params[:headers] is not supplied to Connection#request
10
# @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String
11
# @option params [String] :path Default path; appears after 'scheme://host:port/'. Only used if params[:path] is not supplied to Connection#request
12
# @option params [Fixnum] :port The port on which to connect, to the destination host
13
# @option params [Hash] :query Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request
14
# @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
15
# @option params [String] :proxy Proxy server; e.g. 'http://myproxy.com:8888'
16
# @option params [Fixnum] :retry_limit Set how many times we'll retry a failed request. (Default 4)
17
# @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
18
# @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
19
def initialize(url, params = {})
21
@connection = Excon.defaults.merge({
24
:port => uri.port.to_s,
26
:scheme => uri.scheme,
28
# merge does not deep-dup, so make sure headers is not the original
29
@connection[:headers] = @connection[:headers].dup
33
# use proxy from the environment if present
34
if ENV.has_key?('http_proxy')
35
@proxy = setup_proxy(ENV['http_proxy'])
36
elsif params.has_key?(:proxy)
37
@proxy = setup_proxy(params[:proxy])
40
if @connection[:scheme] == HTTPS
41
# use https_proxy if that has been specified
42
if ENV.has_key?('https_proxy')
43
@proxy = setup_proxy(ENV['https_proxy'])
48
@connection[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
51
# Use Basic Auth if url contains a login
52
if uri.user || uri.password
53
auth = ["#{uri.user}:#{uri.password}"].pack('m').delete("\r\n")
54
@connection[:headers]['Authorization'] ||= "Basic #{auth}"
57
@socket_key = '' << @connection[:host] << ':' << @connection[:port]
61
# Sends the supplied request to the destination host.
62
# @yield [chunk] @see Response#self.parse
63
# @param [Hash<Symbol, >] params One or more optional params, override defaults set in Connection.new
64
# @option params [String] :body text to be sent over a socket
65
# @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
66
# @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String
67
# @option params [String] :path appears after 'scheme://host:port/'
68
# @option params [Fixnum] :port The port on which to connect, to the destination host
69
# @option params [Hash] :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
70
# @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
71
def request(params, &block)
72
# connection has defaults, merge in new params to override
73
params = @connection.merge(params)
74
params[:headers] = @connection[:headers].merge(params[:headers] || {})
75
params[:headers]['Host'] ||= '' << params[:host] << ':' << params[:port]
77
# if path is empty or doesn't start with '/', insert one
78
unless params[:path][0, 1] == '/'
79
params[:path].insert(0, '/')
82
if params.has_key?(:instrumentor)
83
if (retries_remaining ||= params[:retry_limit]) < params[:retry_limit]
84
event_name = "#{params[:instrumentor_name]}.retry"
86
event_name = "#{params[:instrumentor_name]}.request"
88
params[:instrumentor].instrument(event_name, params) do
89
request_kernel(params, &block)
92
request_kernel(params, &block)
94
rescue => request_error
95
if params[:idempotent] && [Excon::Errors::SocketError,
96
Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex }
97
retries_remaining ||= params[:retry_limit]
98
retries_remaining -= 1
99
if retries_remaining > 0
100
if params[:body].respond_to?(:pos=)
101
params[:body].pos = 0
105
if params.has_key?(:instrumentor)
106
params[:instrumentor].instrument("#{params[:instrumentor_name]}.error", :error => request_error)
111
if params.has_key?(:instrumentor)
112
params[:instrumentor].instrument("#{params[:instrumentor_name]}.error", :error => request_error)
119
(old_socket = sockets.delete(@socket_key)) && old_socket.close
122
# Generate HTTP request verb methods
123
Excon::HTTP_VERBS.each do |method|
125
def #{method}(params={}, &block)
126
request(params.merge!(:method => :#{method}), &block)
131
def retry_limit=(new_retry_limit)
132
puts("Excon::Connection#retry_limit= is deprecated, pass :retry_limit to the initializer (#{caller.first})")
133
@connection[:retry_limit] = new_retry_limit
137
puts("Excon::Connection#retry_limit is deprecated, pass :retry_limit to the initializer (#{caller.first})")
138
@connection[:retry_limit] ||= DEFAULT_RETRY_LIMIT
143
def request_kernel(params, &block)
145
response = if params[:mock]
146
invoke_stub(params, &block)
148
socket.params = params
149
# start with "METHOD /path"
150
request = params[:method].to_s.upcase << ' '
152
request << params[:scheme] << '://' << params[:host] << ':' << params[:port]
154
request << params[:path]
156
# add query to path, if there is one
159
request << '?' << params[:query]
162
for key, values in params[:query]
164
request << key.to_s << '&'
166
for value in [*values]
167
request << key.to_s << '=' << CGI.escape(value.to_s) << '&'
171
request.chop! # remove trailing '&'
174
# finish first line with "HTTP/1.1\r\n"
177
# calculate content length and set to handle non-ascii
178
unless params[:headers].has_key?('Content-Length')
179
# The HTTP spec isn't clear on it, but specifically, GET requests don't usually send bodies;
180
# if they don't, sending Content-Length:0 can cause issues.
181
unless (params[:method].to_s.casecmp('GET') == 0 && params[:body].nil?)
182
params[:headers]['Content-Length'] = case params[:body]
184
params[:body].binmode
185
File.size(params[:body])
188
params[:body].force_encoding('BINARY')
197
# add headers to request
198
for key, values in params[:headers]
199
for value in [*values]
200
request << key.to_s << ': ' << value.to_s << CR_NL
204
# add additional "\r\n" to indicate end of headers
207
# write out the request, sans body
208
socket.write(request)
211
unless params[:body].nil? || params[:body].empty?
212
if params[:body].is_a?(String)
213
socket.write(params[:body])
215
while chunk = params[:body].read(CHUNK_SIZE)
222
response = Excon::Response.parse(socket, params, &block)
224
if response.headers['Connection'] == 'close'
230
rescue Excon::Errors::StubNotFound => stub_not_found
231
raise(stub_not_found)
232
rescue => socket_error
234
raise(Excon::Errors::SocketError.new(socket_error))
237
if params.has_key?(:expects) && ![*params[:expects]].include?(response.status)
239
raise(Excon::Errors.status_error(params, response))
245
def invoke_stub(params)
246
block_given = block_given?
247
params[:captures] = {:headers => {}} # setup data to hold captures
248
for stub, response in Excon.stubs
249
headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
250
case value = stub[:headers][key]
252
if match = value.match(params[:headers][key])
253
params[:captures][:headers][key] = match.captures
257
value == params[:headers][key]
260
non_headers_match = (stub.keys - [:headers]).all? do |key|
261
case value = stub[key]
263
if match = value.match(params[key])
264
params[:captures][key] = match.captures
271
if headers_match && non_headers_match
272
response_attributes = case response
274
response.call(params)
279
# don't pass stuff into a block if there was an error
280
if params[:expects] && ![*params[:expects]].include?(response_attributes[:status])
284
if block_given && response_attributes.has_key?(:body)
285
body = response_attributes.delete(:body)
286
content_length = remaining = body.bytesize
288
while i < body.length
289
yield(body[i, CHUNK_SIZE], [remaining - CHUNK_SIZE, 0].max, content_length)
290
remaining -= CHUNK_SIZE
294
return Excon::Response.new(response_attributes)
297
# if we reach here no stubs matched
298
raise(Excon::Errors::StubNotFound.new('no stubs matched ' << params.inspect))
302
sockets[@socket_key] ||= if @connection[:scheme] == HTTPS
303
Excon::SSLSocket.new(@connection, @proxy)
305
Excon::Socket.new(@connection, @proxy)
310
Thread.current[:_excon_sockets] ||= {}
313
def setup_proxy(proxy)
314
uri = URI.parse(proxy)
315
unless uri.host and uri.port and uri.scheme
316
raise Excon::Errors::ProxyParseError, "Proxy is invalid"
318
{:host => uri.host, :port => uri.port, :scheme => uri.scheme}