lib/github_api/response/follow_redirects.rb
require 'faraday'
require 'set'
# First saw on octokit, then copied from lostisland/faraday_middleware
# and adapted for this library.
#
# faraday_middleware/lib/faraday_middleware/response/follow_redirects.rb
module Github
# Public: Exception thrown when the maximum amount of requests is exceeded.
class RedirectLimitReached < Faraday::ClientError
attr_reader :response
def initialize(response)
super "too many redirects; last one to: #{response['location']}"
@response = response
end
end
# Public: Follow HTTP 301, 302, 303, 307, and 308 redirects.
#
# For HTTP 301, 302, and 303, the original GET, POST, PUT, DELETE, or PATCH
# request gets converted into a GET. With `:standards_compliant => true`,
# however, the HTTP method after 301/302 remains unchanged. This allows you
# to opt into HTTP/1.1 compliance and act unlike the major web browsers.
#
# This middleware currently only works with synchronous requests; i.e. it
# doesn't support parallelism.
#
# If you wish to persist cookies across redirects, you could use
# the faraday-cookie_jar gem:
#
# Faraday.new(:url => url) do |faraday|
# faraday.use FaradayMiddleware::FollowRedirects
# faraday.use :cookie_jar
# faraday.adapter Faraday.default_adapter
# end
class Response::FollowRedirects < Faraday::Middleware
# HTTP methods for which 30x redirects can be followed
ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete]
# HTTP redirect status codes that this middleware implements
REDIRECT_CODES = Set.new [301, 302, 303, 307, 308]
# Keys in env hash which will get cleared between requests
ENV_TO_CLEAR = Set.new [:status, :response, :response_headers]
# Default value for max redirects followed
FOLLOW_LIMIT = 3
# Regex that matches characters that need to be escaped in URLs, sans
# the "%" character which we assume already represents an escaped sequence.
URI_UNSAFE = /[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]%]/
# Public: Initialize the middleware.
#
# options - An options Hash (default: {}):
# :limit - A Numeric redirect limit (default: 3)
# :standards_compliant - A Boolean indicating whether to respect
# the HTTP spec when following 301/302
# (default: false)
# :callback - A callable that will be called on redirects
# with the old and new envs
def initialize(app, options = {})
super(app)
@options = options
@convert_to_get = Set.new [303]
@convert_to_get << 301 << 302 unless standards_compliant?
end
def call(env)
perform_with_redirection(env, follow_limit)
end
private
def convert_to_get?(response)
![:head, :options].include?(response.env[:method]) &&
@convert_to_get.include?(response.status)
end
def perform_with_redirection(env, follows)
request_body = env[:body]
response = @app.call(env)
response.on_complete do |response_env|
if follow_redirect?(response_env, response)
raise RedirectLimitReached, response if follows.zero?
new_request_env = update_env(response_env.dup, request_body, response)
callback.call(response_env, new_request_env) if callback
response = perform_with_redirection(new_request_env, follows - 1)
end
end
response
end
def update_env(env, request_body, response)
env[:url] += safe_escape(response['location'])
if convert_to_get?(response)
env[:method] = :get
env[:body] = nil
else
env[:body] = request_body
end
ENV_TO_CLEAR.each {|key| env.delete key }
env
end
def follow_redirect?(env, response)
ALLOWED_METHODS.include? env[:method] and
REDIRECT_CODES.include? response.status
end
def follow_limit
@options.fetch(:limit, FOLLOW_LIMIT)
end
def standards_compliant?
@options.fetch(:standards_compliant, false)
end
def callback
@options[:callback]
end
# Internal: escapes unsafe characters from an URL which might be a path
# component only or a fully qualified URI so that it can be joined onto an
# URI:HTTP using the `+` operator. Doesn't escape "%" characters so to not
# risk double-escaping.
def safe_escape(uri)
uri = uri.split('#')[0] # we want to remove the fragment if present
uri.to_s.gsub(URI_UNSAFE) { |match|
'%' + match.unpack('H2' * match.bytesize).join('%').upcase
}
end
end
end