[RUBY] I tried to understand how the rails method "redirect_to" is defined

This is the second project to learn how to write ruby by interpreting the definition of rails method. The theme this time is redirect_to

Definition of redirect_to

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

The first line

def redirect_to(options = {}, response_status = {})

Takes two arguments. In both cases, an empty hash is assigned if no arguments are passed.

2nd line

raise ActionControllerError.new("Cannot redirect to nil!") unless options

Raise error when opsions are empty. By the way, ruby treats everything except false and nil as true, so an empty hash {} will not cause an error. An error will occur if the user dares to assign nil / false.

3rd line

raise AbstractController::DoubleRenderError if response_body

An error will occur if you have already received an HTTP response. I don't know how response_body is assigned here, but it seems to return an error when multiple HTTP communications are exchanged at the same time.

4th line

self.status = _extract_redirect_to_status(options, response_status)

A new method has come out this time as well.

\ _extract_redirect_to_status method

Definition

#Fileactionpack/lib/action_controller/metal/redirecting.rb, line 117
def _extract_redirect_to_status(options, response_status)
  if options.is_a?(Hash) && options.key?(:status)
    Rack::Utils.status_code(options.delete(:status))
  elsif response_status.key?(:status)
    Rack::Utils.status_code(response_status[:status])
  else
    302
  end
end

2nd line

if options.is_a?(Hash) && options.key?(:status)

One item of if condition is_a? Method is the type of the receiver (here "options") written in parentheses. A method that returns true if it matches (here "hash").

The two items key? of the if condition are the keys whose names are written in parentheses. A method that returns true if it has.

In other words, if the argument passed to redirect_to is a hash and has the key status, the contents of the if statement will be executed. As you can see in the Rails docs mentioned at the beginning

redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }

Status is a parameter set by the programmer who uses the redirect_to method. As you read, you can see what role the parameters play.

3rd line

Rack::Utils.status_code(options.delete(:status))

Rack is a group of programs that act as an intermediary between Rails applications and application servers. (Called middleware) status_code is a method defined in a library called Utils.

The value assigned to the key status of options by delete method is passed to the status_code method.

status_code method

def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE[status] || 500
  else
    status.to_i
  end
end

This method is just a method to display the value of status. It is called SYMBOL_TO_STATUS_CODE only when it is passed by Symbol (determined by is_a on the second line). The conversion hash defined in Rack :: Utils is used.

The definition itself

SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
  [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]

It looks like this. The hash HTTP_STATUS_CODES also defined by this code in Rack :: Utils is {: HTTP status name Convert to a hash array called => HTTP Status Code}.

In particular

 pry(main)> Rack::Utils::SYMBOL_TO_STATUS_CODE
=> {:continue=>100,
 :switching_protocols=>101,
 :processing=>102,
 :early_hints=>103,
 :ok=>200,
 :created=>201,
 :accepted=>202,
 :non_authoritative_information=>203,
 :no_content=>204,
 :reset_content=>205,
 :partial_content=>206,
 :multi_status=>207,
 :already_reported=>208,
 :im_used=>226,
 :multiple_choices=>300,
 :moved_permanently=>301,
 :found=>302,
 :see_other=>303,
 :not_modified=>304,
 :use_proxy=>305,
 :"(unused)"=>306,
 :temporary_redirect=>307,
 :permanent_redirect=>308,
:bad_request=>400,
 :unauthorized=>401,
 :payment_required=>402,
 :forbidden=>403,
 :not_found=>404,
 :method_not_allowed=>405,
 :not_acceptable=>406,
 :proxy_authentication_required=>407,
 :request_timeout=>408,
 :conflict=>409,
 :gone=>410,
 :length_required=>411,
 :precondition_failed=>412,
 :payload_too_large=>413,
 :uri_too_long=>414,
 :unsupported_media_type=>415,
 :range_not_satisfiable=>416,
 :expectation_failed=>417,
 :misdirected_request=>421,
 :unprocessable_entity=>422,
 :locked=>423,
 :failed_dependency=>424,
 :too_early=>425,
 :upgrade_required=>426,
 :precondition_required=>428,
 :too_many_requests=>429,
 :request_header_fields_too_large=>431,
 :unavailable_for_legal_reasons=>451,
 :internal_server_error=>500,
 :not_implemented=>501,
 :bad_gateway=>502,
 :service_unavailable=>503,
 :gateway_timeout=>504,
 :http_version_not_supported=>505,
 :variant_also_negotiates=>506,
 :insufficient_storage=>507,
 :loop_detected=>508,
 :bandwidth_limit_exceeded=>509,
 :not_extended=>510,
 :network_authentication_required=>511}
#Run on the Rails console.

In other words, when you return to the definition of status_code

SYMBOL_TO_STATUS_CODE[status] || 500

When status:: not_found is set in, "404" is returned. If there is no corresponding item, "500" is returned.

If a value such as "404" is displayed in options [: status] instead of symbol, the 4th to 5th lines will return the value as it is in integer format.

else
  status.to_i
end

\ _extract_redirect_to_status method

(Repost)

#Fileactionpack/lib/action_controller/metal/redirecting.rb, line 117
def _extract_redirect_to_status(options, response_status)
  if options.is_a?(Hash) && options.key?(:status)
    Rack::Utils.status_code(options.delete(:status))
  elsif response_status.key?(:status)
    Rack::Utils.status_code(response_status[:status])
  else
    302
  end
end

So far, I've read what I'm doing on the third line.

4th line

elsif response_status.key?(:status)

The key? method makes this conditional statement true when the argument response_status has a key status. The method to be executed is the same as before.

It seems to be an if branch to allow the HTTP status to be returned correctly regardless of whether the status value is passed in options or response_status.

If the HTTP status to be returned is not passed The else statement on lines 6-7 returns 302. (It means "redirect processing".)

redirect_to (Repost)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

In the 4th line, I decided the HTTP status code to be returned to the user side.

5th line

self.location = _compute_redirect_to_location(request, options)

A new rails method has arrived. The request assigned here is the object that is created every time on the controller. Each parameter of the HTTP request sent from the user side to the rails application side is stored.

What kind of parameters are there? [Rails Guide](https://railsguides.jp/action_controller_overview.html#request%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3 % 83% 88% E3% 81% A8response% E3% 82% AA% E3% 83% 96% E3% 82% B8% E3% 82% A7% E3% 82% AF% E3% 83% 88) I am.

You can retrieve the value like request.host.

The definition itself is made on the ActionDispatch :: Request model. The methods available for this model are listed here (https://api.rubyonrails.org/classes/ActionDispatch/Request.html).

\ _compute_redirect_to_location method

Definition

# File actionpack/lib/action_controller/metal/redirecting.rb, line 96
    def _compute_redirect_to_location(request, options) #:nodoc:
      case options
      # The scheme name consist of a letter followed by any combination of
      # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
      # characters; and is terminated by a colon (":").
      # See https://tools.ietf.org/html/rfc3986#section-3.1
      # The protocol relative scheme starts with a double slash "//".
      when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
        options
      when String
        request.protocol + request.host_with_port + options
      when Proc
        _compute_redirect_to_location request, instance_eval(&options)
      else
        url_for(options)
      end.delete("\00\\r\n")
    end

This method will generate the redirect URL. Conditional branching by case statement is done by the contents of opsions.

Lines 9-10

when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
  options

http://のように先頭に○○://とつく文字列のときに、この条件がtrueとなります。 Since it is judged that it is in the body of the URL, the value of options is returned as it is.

Lines 11-12

when String
  request.protocol + request.host_with_port + options

Called when options is a string. The URL is generated based on the request object that stores the request information from the user side introduced earlier. http: // and https: // are stored in the protocol. host_with_port will generate hosts after //. Finally, options are given.

The host_with_port method is a simple method that calls the properties = host and port_string stored in the request object and combines them as a string.

# File actionpack/lib/action_dispatch/http/url.rb, line 250
def host_with_port
  "#{host}#{port_string}"
end

Lines 13-14

when Proc
  _compute_redirect_to_location request, instance_eval(&options)

What is Proc

A procedural object that objectifies a block with a context (local variable scope or stack frame). (https://docs.ruby-lang.org/ja/latest/class/Proc.html)[https://docs.ruby-lang.org/ja/latest/class/Proc.html]

redirect_to can be used in block format, and it is OK to recognize that it will enter this branch when a block is given.

For example

get 'jokes/:number', to: redirect { |params, request|
  path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
  "http://#{request.host_with_port}/#{path}"
  }

You can write something like this.

When the block is called, the _compute_redirect_to_location method is called recursively. For beginners, recursion is the calling of the same method within a method. For example

def method
  method
end

It is shaped like. (In this case, it will be an infinite loop. Normally, it is written so that it does not loop infinitely with if branch etc.)

(Repost)

\_compute_redirect_to_location request, instance_eval(&options)

instance_eval (& options) is assigned to the second term of the \ _compute_redirect_to_location (request, options) method.

There is a & in front of options, which means to assign a block. instance_eval is a method that returns the output when the code in the block is executed.

Let's consider the previous example (Repost)

get 'jokes/:number', to: redirect { |params, request|
  path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
  "http://#{request.host_with_port}/#{path}"
  }

In this case, the code on the 2nd and 3rd lines will be executed, and as a result, the URL starting with http: // on the 3rd line will be returned as a string. In other words The URL is assigned to the \ _compute_redirect_to_location method. The case statement is called and branches to the conditions on the 9th to 10th lines mentioned above. (Repost)

when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
  options

Lines 15-16

else
  url_for(options)

To summarize the conditions so far http://~といった具体的なURL、ファイル名を指す文字列、ブロックについて分岐されてきたが、いずれにも該当しない場合はこのelse文の内容が実行されることになります。

What comes out here is the url_for method that was also used in link_to.

def url_for(options)
    if options[:only_path]
      path_for options
    else
      full_url_for options
    end
end
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080'
# => 'http://somehost.org:8080/tasks/testing'
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true
# => '/tasks/testing#ok'
url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true
# => 'http://somehost.org/tasks/testing/'
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33'
# => 'http://somehost.org/tasks/testing?number=33'

(Quote) Generate a URL like this.

17th line

end.delete("\00\\r\n")

Delete is called at the end of the case statement.

Since it is called for a character string, the behavior is different from the delete that has appeared so far. According to the Ruby reference

delete(*strs) -> String Generates and returns a string with the characters contained in strs removed.

The reference also includes a reference article for the strs format. I'm not sure because it's less important and I don't understand it, Since \ r and \ n are special characters that represent "return" and "line feed", respectively, it is presumed that they have the meaning of deleting these characters that are mixed in the url. Then I don't know what \ 00 \ is, and maybe I can separate it as \ 0, 0, \, r, \ n.

So far, the \ _compute_redirect_to_location method.

redirect_to (Repost)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

The fourth line determines the HTTP status code to return to the user after the redirect. The URL of the redirect destination was decided on the 5th line.

6th line

self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"

Now, decide the html data to be returned to the user side. Actually, the redirect process is performed based on the value of location, so this html will not be displayed on the user browser.

Here, the value stored in self. ~ Is passed to the browser on the user side as an HTTP response. Looking at the URL of the location property (attribute) in the HTTP header, the user's browser makes the request again.

See here for the browser processing mechanism.

Summary

(Repost)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

When redirect_to is called

  1. Error if options include nil
  2. Error if response has already been generated
  3. Store the status of the HTTP response returned to the user (200: success, 404: Not Found error, etc.) in the response object.
  4. Store the redirect URL in the response object
  5. Store HTML data in response object (but basically never displayed)

Impressions

I found that it is the role of generating the response data necessary for that, assuming that the actual processing is left to the client (browser used by the user) rather than performing the processing in this method.

If you deepen your understanding of the processing performed by Rails applications and middleware, it seems that you will be able to implement more complicated processing.

Recommended Posts

I tried to understand how the rails method "redirect_to" is defined
I tried to understand how the rails method "link_to" is defined
I tried to explain the method
[Rails] How to use the map method
[Rails] I tried to raise the Rails version from 5.0 to 5.2
[Rails] I don't know how to use the model ...
I tried to introduce Bootstrap 4 to the Rails 6 app [for beginners]
[Rails] I tried using the button_to method for the first time
How to use the link_to method
How to use the include? method
How to use the form_with method
[Rails] I tried deleting the application
I tried to understand nil guard
I tried to sort the data in descending order, ascending order / Rails
I tried to implement the image preview function with Rails / jQuery
[Rails] How to omit the display of the character string of the link_to method
[Java] I tried to make a maze by the digging method ♪
I examined the concept of the process to understand how Docker works
How to write the view when Vue is introduced in Rails?
I tried to summarize the methods used
I tried to introduce CircleCI 2.0 to Rails app
[Java] How to use the toString () method
I wanted to add @VisibleForTesting to the method
I was addicted to the roll method
I tried to implement the Iterator pattern
I tried to summarize the Stream API
[Ruby on Rails] How to use redirect_to
What is Docker? I tried to summarize
Method definition location Summary of how to check When defined in the project and Rails / Gem
[Rails] How to use helper method, confimartion
[Introduction to Java] I tried to summarize the knowledge that I think is essential
[Rails] How to solve the time lag of created_at after save method
[Rails] How to decide the destination by "rails routes"
Output of how to use the slice method
The code I used to connect Rails 3 to PostgreSQL 10
How to use the replace () method (Java Silver)
I tried to set tomcat to run the Servlet.
[Ruby on Rails] How to use session method
How to check Rails commands in the terminal
[Ruby basics] How to use the slice method
[Rails / ActiveRecord] I want to validate the value before the type is converted (_before_type_cast)
[JavaScript] The strongest case when I tried to summarize the parts I do not understand
[Rails] How to operate the helper method used in the main application with Administrate
[JDBC ③] I tried to input from the main method using placeholders and arguments.
[Rails] How to connect to an external API using HTTP Client (I tried connecting to Qiita API)
How to set the display time to Japan time in Rails
How far is the correct answer to divide the process?
[rails] How to use devise helper method before_action: authenticate_user!
How to write Rails
I tried to summarize the state transition of docker
I tried to decorate the simple calendar a little
[Rails] I tried playing with the comment send button
[Ruby on Rails] How to change the column name
05. I tried to stub the source of Spring Boot
I tried to reduce the capacity of Spring Boot
[Rails] How to change the column name of the table
How to uninstall Rails
I tried to make FizzBuzz that is uselessly flexible
Rails6 I tried to introduce Docker to an existing application
I want to call the main method using reflection
[Rails] How to get the contents of strong parameters