class Yt::Request
@private A wrapper around Net::HTTP to send HTTP requests to any web API and return their result or raise an error if the result is unexpected. The basic way to use Request
is by calling run
on an instance. @example List the most popular videos on YouTube.
host = 'www.googleapis.com' path = '/youtube/v3/videos' params = {chart: 'mostPopular', key: ENV['API_KEY'], part: 'snippet'} response = Yt::Request.new(path: path, params: params).run response.body['items'].map{|video| video['snippet']['title']}
Public Class Methods
Source
# File lib/yt/request.rb, line 52 def initialize(options = {}) @method = options.fetch :method, :get @expected_response = options.fetch :expected_response, Net::HTTPSuccess @response_format = options.fetch :response_format, :json @host = options[:host] @path = options[:path] @params = options.fetch :params, {} # Note: This is to be invoked by auth-only YouTube APIs. @params[:key] = options[:api_key] if options[:api_key] # Note: This is to be invoked by all YouTube API except Annotations, # Analyitics and Uploads camelize_keys! @params if options.fetch(:camelize_params, true) @request_format = options.fetch :request_format, :json @body = options[:body] @headers = options.fetch :headers, {} @auth = options[:auth] end
Initializes a Request
object. @param [Hash] options the options for the request. @option options [String, Symbol] :method (:get) The HTTP method to use. @option options [Class] :expected_response (Net::HTTPSuccess) The class
of response that the request should obtain when run.
@option options [String, Symbol] :response_format (:json) The expected
format of the response body. If passed, the response body will be parsed according to the format before being returned.
@option options [String] :host The host component of the request URI. @option options [String] :path The path component of the request URI. @option options [Hash] :params ({}) The params to use as the query
component of the request URI, for instance the Hash +{a: 1, b: 2}+ corresponds to the query parameters "a=1&b=2".
@option options [Hash] :camelize_params (true) whether to transform
each key of params into a camel-case symbol before sending the request.
@option options [Hash] :request_format (:json) The format of the
request body. If a request body is passed, it will be parsed according to this format before sending it in the request.
@option options [#size] :body The body component of the request. @option options [Hash] :headers ({}) The headers component of the
request.
@option options [#access_token, refreshed_access_token?] :auth The
authentication object. If set, must respond to +access_token+ and return the OAuth token to make an authenticated request, and must respond to +refreshed_access_token?+ and return whether the access token can be refreshed if expired.
Public Instance Methods
Source
# File lib/yt/request.rb, line 90 def as_curl 'curl'.tap do |curl| curl << " -X #{http_request.method}" http_request.each_header{|k, v| curl << %Q{ -H "#{k}: #{v}"}} curl << %Q{ -d '#{http_request.body}'} if http_request.body curl << %Q{ "#{uri.to_s}"} end end
Returns the cURL
version of the request, useful to re-run the request in a shell terminal. @return [String] the cURL
version of the request.
Source
# File lib/yt/request.rb, line 77 def run if matches_expectations? response.tap{parse_response!} elsif run_again? run else raise response_error, error_message.to_json end end
Sends the request and returns the response. If the request fails once for a temporary server error or an expired token, tries the request again before eventually raising an error. @return [Net::HTTPResponse] if the request succeeds and matches the
expectations, the response with the body appropriately parsed.
@raise [Yt::RequestError] if the request fails or the response does
not match the expectations.
Private Instance Methods
Source
# File lib/yt/request.rb, line 130 def camelize_keys!(hash) hash.keys.each do |key| hash[key.to_s.camelize(:lower).to_sym] = hash.delete key end if hash.is_a? Hash hash end
Destructively converts all the keys of hash to camel-case symbols. Note: This is to be invoked by all YouTube API except Accounts
Source
# File lib/yt/request.rb, line 287 def error_message response_body = JSON(response.body) rescue response.inspect {request_curl: as_curl, response_body: response_body} end
Return the elements of the request/response that are worth displaying as an error message if the request fails. If the response format is JSON, showing the parsed body is sufficient, otherwise the whole (inspected) response is worth looking at.
Source
# File lib/yt/request.rb, line 274 def exceeded_quota? response_error == Errors::Forbidden && response.body =~ /Exceeded/i end
@return [Boolean] whether the request exceeds the YouTube quota
Source
# File lib/yt/request.rb, line 220 def extra_server_errors if defined? OpenSSL::SSL::SSLErrorWaitReadable [OpenSSL::SSL::SSLErrorWaitReadable] else [] end end
Returns the list of server errors that are only raised (and therefore can only be rescued) by specific versions of Ruby. @see: github.com/Fullscreen/yt/pull/110
Source
# File lib/yt/request.rb, line 109 def http_request net_http_class = "Net::HTTP::#{@method.capitalize}".constantize @http_request ||= net_http_class.new(uri.request_uri).tap do |request| set_request_body! request set_request_headers! request end end
@return [Net::HTTPRequest] the full HTTP request object,
inclusive of headers of request body.
Source
# File lib/yt/request.rb, line 157 def matches_expectations? response.is_a? @expected_response end
@return [Boolean] whether the class of response returned by running
the request matches the expected class of response.
Source
# File lib/yt/request.rb, line 183 def parse_response! response.body = case @response_format when :xml then Hash.from_xml response.body when :json then JSON response.body end if response.body end
Replaces the body of the response with the parsed version of the body, according to the format specified in the Request
.
Source
# File lib/yt/request.rb, line 248 def refresh_token_and_retry? if unauthorized? && @auth && @auth.refreshed_access_token? @response = @http_request = @uri = nil true end rescue Errors::MissingAuth false end
In case an authorized request responds with “Unauthorized”, checks if the original access token can be refreshed. If that’s the case, clears the memoized variables and returns true, so the request can be run again, otherwise raises an error.
Source
# File lib/yt/request.rb, line 162 def response @response ||= send_http_request rescue *server_errors => e @response ||= e end
Run the request and memoize the response or the server error received.
Source
# File lib/yt/request.rb, line 259 def response_error case response when *server_errors then Errors::ServerError when Net::HTTPUnauthorized then Errors::Unauthorized when Net::HTTPForbidden then Errors::Forbidden else Errors::RequestError end end
@return [Yt::RequestError] the error associated to the class of the
response.
Source
# File lib/yt/request.rb, line 240 def retry_time 3 + (10 * @retries_so_far) end
Source
# File lib/yt/request.rb, line 198 def run_again? refresh_token_and_retry? && sleep_and_retry?(1) || server_error? && sleep_and_retry?(3) end
Returns whether it is worth to run a failed request again. There are three cases in which retrying a request might be worth:
-
when the server specifies that the request token has expired and the user has to refresh the token in order to try again
-
when the server is unreachable, and waiting for a couple of seconds might solve the connection issues.
-
when the user has reached the quota for requests/second, and waiting for a couple of seconds might solve the connection issues.
Source
# File lib/yt/request.rb, line 170 def send_http_request net_http_options = [uri.host, uri.port, use_ssl: true] ActiveSupport::Notifications.instrument 'request.yt' do |payload| payload[:method] = @method payload[:request_uri] = uri payload[:response] = Net::HTTP.start(*net_http_options) do |http| http.request http_request end end end
Send the request to the server, allowing ActiveSupport::Notifications client to subscribe to the request.
Source
# File lib/yt/request.rb, line 269 def server_error? response_error == Errors::ServerError end
@return [Boolean] whether the response matches any server error.
Source
# File lib/yt/request.rb, line 204 def server_errors [ OpenSSL::SSL::SSLError, Errno::ETIMEDOUT, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ECONNRESET, Net::OpenTimeout, SocketError, Net::HTTPServerError ] + extra_server_errors end
Returns the list of server errors worth retrying the request once.
Source
# File lib/yt/request.rb, line 120 def set_request_body!(request) case @request_format when :json then request.body = (camelize_keys! @body).to_json when :form then request.set_form_data @body when :file then request.body_stream = @body end if @body end
Adds the request body to the request in the appropriate format. if the request body is a JSON Object, transform its keys into camel-case, since this is the common format for JSON APIs.
Source
# File lib/yt/request.rb, line 141 def set_request_headers!(request) case @request_format when :json request.initialize_http_header 'Content-Type' => 'application/json' request.initialize_http_header 'Content-length' => '0' unless @body when :file request.initialize_http_header 'Content-Length' => @body.size.to_s request.initialize_http_header 'Transfer-Encoding' => 'chunked' end @headers['User-Agent'] = 'Yt::Request (gzip)' @headers['Authorization'] = "Bearer #{@auth.access_token}" if @auth @headers.each{|name, value| request.add_field name, value} end
Adds the request headers to the request in the appropriate format. The User-Agent header is also set to recognize the request, and to tell the server that gzip compression can be used, since Net::HTTP supports it and automatically sets the Accept-Encoding header.
Source
# File lib/yt/request.rb, line 231 def sleep_and_retry?(max_retries = 1) @retries_so_far ||= -1 @retries_so_far += 1 if (@retries_so_far < max_retries) @response = @http_request = @uri = nil sleep retry_time end end
Sleeps for a while and returns true for the first max_retries
times, then returns false. Useful to try the same request again multiple times with a delay if a connection error occurs.
Source
# File lib/yt/request.rb, line 102 def uri attributes = {host: @host, path: @path, query: @params.to_param} @uri ||= URI::HTTPS.build attributes end
@return [URI::HTTPS] the (memoized) URI of the request.