class Chef::HTTP
Chef::HTTP¶ ↑
Basic HTTP client, with support for adding features via middleware
Attributes
Public Class Methods
# File lib/chef/http.rb, line 66 def self.middlewares @middlewares ||= [] end
Create a HTTP client object. The supplied
url
is used as the base for all subsequent requests. For
example, when initialized with a base url localhost:4000, a call to get
with 'nodes' will make an HTTP GET request
to localhost:4000/nodes
# File lib/chef/http.rb, line 84 def initialize(url, options={}) @url = url @default_headers = options[:headers] || {} @sign_on_redirect = true @redirects_followed = 0 @redirect_limit = 10 @middlewares = [] self.class.middlewares.each do |middleware_class| @middlewares << middleware_class.new(options) end end
# File lib/chef/http.rb, line 70 def self.use(middleware_class) middlewares << middleware_class end
Public Instance Methods
# File lib/chef/http.rb, line 205 def create_url(path) return path if path.is_a?(URI) if path =~ /^(http|https):\/\// URI.parse(path) elsif path.nil? or path.empty? URI.parse(@url) else # The regular expressions used here are to make sure '@url' does not have # any trailing slashes and 'path' does not have any leading slashes. This # way they are always joined correctly using just one slash. URI.parse(@url.gsub(%r{/+$}, '') + '/' + path.gsub(%r{^/+}, '')) end end
# File lib/chef/http.rb, line 198 def http_client(base_url=nil) base_url ||= url BasicClient.new(base_url) end
This is only kept around to provide access to cache control data in lib/chef/provider/remote_file/http.rb Find a better API.
# File lib/chef/http.rb, line 393 def last_response @last_response end
Makes an HTTP request to path
with the
given method
, headers
, and data
(if
applicable).
# File lib/chef/http.rb, line 139 def request(method, path, headers={}, data=false) url = create_url(path) method, url, headers, data = apply_request_middleware(method, url, headers, data) response, rest_request, return_value = send_http_request(method, url, headers, data) response, rest_request, return_value = apply_response_middleware(response, rest_request, return_value) response.error! unless success_response?(response) return_value rescue Exception => exception log_failed_request(response, return_value) unless response.nil? if exception.respond_to?(:chef_rest_request=) exception.chef_rest_request = rest_request end raise end
Makes a streaming download request, streaming the response body to a tempfile. If a block is given, the tempfile is passed to the block and the tempfile will automatically be unlinked after the block is executed.
If no block is given, the tempfile is returned, which means it's up to you to unlink the tempfile when you're done with it.
# File lib/chef/http.rb, line 162 def streaming_request(path, headers={}, &block) url = create_url(path) response, rest_request, return_value = nil, nil, nil tempfile = nil method = :GET method, url, headers, data = apply_request_middleware(method, url, headers, data) response, rest_request, return_value = send_http_request(method, url, headers, data) do |http_response| if http_response.kind_of?(Net::HTTPSuccess) tempfile = stream_to_tempfile(url, http_response) end apply_stream_complete_middleware(http_response, rest_request, return_value) end return nil if response.kind_of?(Net::HTTPRedirection) unless response.kind_of?(Net::HTTPSuccess) response.error! end if block_given? begin yield tempfile ensure tempfile && tempfile.close! end end tempfile rescue Exception => e log_failed_request(response, return_value) unless response.nil? if e.respond_to?(:chef_rest_request=) e.chef_rest_request = rest_request end raise end
Protected Instance Methods
# File lib/chef/http.rb, line 219 def apply_request_middleware(method, url, headers, data) middlewares.inject([method, url, headers, data]) do |req_data, middleware| Chef::Log.debug("Chef::HTTP calling #{middleware.class}#handle_request") middleware.handle_request(*req_data) end end
# File lib/chef/http.rb, line 226 def apply_response_middleware(response, rest_request, return_value) middlewares.reverse.inject([response, rest_request, return_value]) do |res_data, middleware| Chef::Log.debug("Chef::HTTP calling #{middleware.class}#handle_response") middleware.handle_response(*res_data) end end
# File lib/chef/http.rb, line 233 def apply_stream_complete_middleware(response, rest_request, return_value) middlewares.reverse.inject([response, rest_request, return_value]) do |res_data, middleware| Chef::Log.debug("Chef::HTTP calling #{middleware.class}#handle_stream_complete") middleware.handle_stream_complete(*res_data) end end
# File lib/chef/http.rb, line 332 def config Chef::Config end
# File lib/chef/http.rb, line 336 def follow_redirect raise Chef::Exceptions::RedirectLimitExceeded if @redirects_followed >= redirect_limit @redirects_followed += 1 Chef::Log.debug("Following redirect #{@redirects_followed}/#{redirect_limit}") yield ensure @redirects_followed = 0 end
# File lib/chef/http.rb, line 328 def http_retry_count config[:http_retry_count] end
# File lib/chef/http.rb, line 324 def http_retry_delay config[:http_retry_delay] end
# File lib/chef/http.rb, line 240 def log_failed_request(response, return_value) return_value ||= {} error_message = "HTTP Request Returned #{response.code} #{response.message}: " error_message << (return_value["error"].respond_to?(:join) ? return_value["error"].join(", ") : return_value["error"].to_s) Chef::Log.info(error_message) end
Wraps an HTTP request with retry logic.
Arguments¶ ↑
- url
-
URL of the request, used for error messages
# File lib/chef/http.rb, line 289 def retrying_http_errors(url) http_attempts = 0 begin http_attempts += 1 yield rescue SocketError, Errno::ETIMEDOUT => e e.message.replace "Error connecting to #{url} - #{e.message}" raise e rescue Errno::ECONNREFUSED if http_retry_count - http_attempts + 1 > 0 Chef::Log.error("Connection refused connecting to #{url}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end raise Errno::ECONNREFUSED, "Connection refused connecting to #{url}, giving up" rescue Timeout::Error if http_retry_count - http_attempts + 1 > 0 Chef::Log.error("Timeout connecting to #{url}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end raise Timeout::Error, "Timeout connecting to #{url}, giving up" rescue Net::HTTPFatalError => e if http_retry_count - http_attempts + 1 > 0 sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts) Chef::Log.error("Server returned error for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s") sleep(sleep_time) retry end raise end end
Runs a synchronous HTTP request, with no middleware applied (use request to have the middleware applied). The entire response will be loaded into memory.
# File lib/chef/http.rb, line 253 def send_http_request(method, url, headers, body, &response_handler) headers = build_headers(method, url, headers, body) retrying_http_errors(url) do client = http_client(url) return_value = nil if block_given? request, response = client.request(method, url, body, headers, &response_handler) else request, response = client.request(method, url, body, headers) {|r| r.read_body } return_value = response.read_body end @last_response = response if response.kind_of?(Net::HTTPSuccess) [response, request, return_value] elsif response.kind_of?(Net::HTTPNotModified) # Must be tested before Net::HTTPRedirection because it's subclass. [response, request, false] elsif redirect_location = redirected_to(response) if [:GET, :HEAD].include?(method) follow_redirect do send_http_request(method, create_url(redirect_location), headers, body, &response_handler) end else raise Exceptions::InvalidRedirect, "#{method} request was redirected from #{url} to #{redirect_location}. Only GET and HEAD support redirects." end else [response, request, nil] end end end
# File lib/chef/http.rb, line 247 def success_response?(response) response.kind_of?(Net::HTTPSuccess) || response.kind_of?(Net::HTTPRedirection) end
Private Instance Methods
# File lib/chef/http.rb, line 355 def build_headers(method, url, headers={}, json_body=false) headers = @default_headers.merge(headers) headers['Content-Length'] = json_body.bytesize.to_s if json_body headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers] headers end
# File lib/chef/http.rb, line 348 def redirected_to(response) return nil unless response.kind_of?(Net::HTTPRedirection) # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this return nil if response.kind_of?(Net::HTTPNotModified) response['location'] end
# File lib/chef/http.rb, line 362 def stream_to_tempfile(url, response) tf = Tempfile.open("chef-rest") if Chef::Platform.windows? tf.binmode # required for binary files on Windows platforms end Chef::Log.debug("Streaming download from #{url.to_s} to tempfile #{tf.path}") # Stolen from http://www.ruby-forum.com/topic/166423 # Kudos to _why! stream_handler = StreamHandler.new(middlewares, response) response.read_body do |chunk| tf.write(stream_handler.handle_chunk(chunk)) end tf.close tf rescue Exception tf.close! raise end