class Chef::HTTP

Chef::HTTP

Basic HTTP client, with support for adding features via middleware

Attributes

middlewares[R]
redirect_limit[R]
sign_on_redirect[R]
url[R]

Public Class Methods

middlewares() click to toggle source
# File lib/chef/http.rb, line 66
def self.middlewares
  @middlewares ||= []
end
new(url, options={}) click to toggle source

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
use(middleware_class) click to toggle source
# File lib/chef/http.rb, line 70
def self.use(middleware_class)
  middlewares << middleware_class
end

Public Instance Methods

create_url(path) click to toggle source
# 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
delete(path, headers={}) click to toggle source

Send an HTTP DELETE request to the path

Parameters

path

path part of the request URL

# File lib/chef/http.rb, line 133
def delete(path, headers={})
  request(:DELETE, path, headers)
end
get(path, headers={}) click to toggle source

Send an HTTP GET request to the path

Parameters

path

The path to GET

# File lib/chef/http.rb, line 109
def get(path, headers={})
  request(:GET, path, headers)
end
head(path, headers={}) click to toggle source

Send an HTTP HEAD request to the path

Parameters

path

path part of the request URL

# File lib/chef/http.rb, line 101
def head(path, headers={})
  request(:HEAD, path, headers)
end
http_client(base_url=nil) click to toggle source
# File lib/chef/http.rb, line 198
def http_client(base_url=nil)
  base_url ||= url
  BasicClient.new(base_url)
end
last_response() click to toggle source

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
post(path, json, headers={}) click to toggle source

Send an HTTP POST request to the path

Parameters

path

path part of the request URL

# File lib/chef/http.rb, line 125
def post(path, json, headers={})
  request(:POST, path, headers, json)
end
put(path, json, headers={}) click to toggle source

Send an HTTP PUT request to the path

Parameters

path

path part of the request URL

# File lib/chef/http.rb, line 117
def put(path, json, headers={})
  request(:PUT, path, headers, json)
end
request(method, path, headers={}, data=false) click to toggle source

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
streaming_request(path, headers={}) { |tempfile| ... } click to toggle source

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

apply_request_middleware(method, url, headers, data) click to toggle source
# 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
apply_response_middleware(response, rest_request, return_value) click to toggle source
# 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
apply_stream_complete_middleware(response, rest_request, return_value) click to toggle source
# 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
config() click to toggle source
# File lib/chef/http.rb, line 332
def config
  Chef::Config
end
follow_redirect() { || ... } click to toggle source
# 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
http_retry_count() click to toggle source
# File lib/chef/http.rb, line 328
def http_retry_count
  config[:http_retry_count]
end
http_retry_delay() click to toggle source
# File lib/chef/http.rb, line 324
def http_retry_delay
  config[:http_retry_delay]
end
log_failed_request(response, return_value) click to toggle source
# 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
retrying_http_errors(url) { || ... } click to toggle source

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
send_http_request(method, url, headers, body, &response_handler) click to toggle source

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
success_response?(response) click to toggle source
# 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

build_headers(method, url, headers={}, json_body=false) click to toggle source
# 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
redirected_to(response) click to toggle source
# 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
stream_to_tempfile(url, response) click to toggle source
# 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