lib/ridley/middleware/follow_redirects.rb
require 'set'
module Ridley
module Middleware
# Borrowed and modified from:
# {https://github.com/lostisland/faraday_middleware/blob/master/lib/faraday_middleware/response/follow_redirects.rb}
#
# Public: Follow HTTP 301, 302, 303, and 307 redirects for GET, PATCH, POST,
# PUT, and DELETE requests.
#
# This middleware does not follow the HTTP specification for HTTP 302, by
# default, in that it follows the improper implementation used by most major
# web browsers which forces the redirected request to become a GET request
# regardless of the original request method.
#
# For HTTP 301, 302, and 303, the original request is transformed into a
# GET request to the response Location, by default. However, with standards
# compliance enabled, a 302 will instead act in accordance with the HTTP
# specification, which will replay the original request to the received
# Location, just as with a 307.
#
# For HTTP 307, the original request is replayed to the response Location,
# including original HTTP request method (GET, POST, PUT, DELETE, PATCH),
# original headers, and original body.
#
# This middleware currently only works with synchronous requests; in other
# words, it doesn't support parallelism.
class FollowRedirects < Faraday::Middleware
include Ridley::Logging
# 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]
# 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
# 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 302
# (default: false)
# cookie - Use either an array of strings
# (e.g. ['cookie1', 'cookie2']) to choose kept cookies
# or :all to keep all cookies.
def initialize(app, options = {})
super(app)
@options = options
@replay_request_codes = Set.new [307]
@replay_request_codes << 302 if standards_compliant?
end
def call(env)
perform_with_redirection(env, follow_limit)
end
private
def perform_with_redirection(env, follows)
request_body = env[:body]
response = @app.call(env)
response.on_complete do |env|
if follow_redirect?(env, response)
log.debug { "==> request redirected to #{response['location']}" }
log.debug { "request env: #{env}" }
if follows.zero?
log.debug { "==> too many redirects" }
raise Ridley::Errors::RedirectLimitReached, response
end
env = update_env(env, request_body, response)
response = perform_with_redirection(env, follows - 1)
end
end
response
end
def update_env(env, request_body, response)
env[:url] += response['location']
if @options[:cookies]
cookies = keep_cookies(env)
env[:request_headers][:cookies] = cookies unless cookies.nil?
end
env[:body] = request_body
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 keep_cookies(env)
cookies = @options.fetch(:cookies, [])
response_cookies = env[:response_headers][:cookies]
cookies == :all ? response_cookies : selected_request_cookies(response_cookies)
end
def selected_request_cookies(cookies)
selected_cookies(cookies)[0...-1]
end
def selected_cookies(cookies)
"".tap do |cookie_string|
@options[:cookies].each do |cookie|
string = /#{cookie}=?[^;]*/.match(cookies)[0] + ';'
cookie_string << string
end
end
end
def standards_compliant?
@options.fetch(:standards_compliant, false)
end
end
end
end
Faraday::Response.register_middleware follow_redirects: Ridley::Middleware::FollowRedirects