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