Rails rescue error

Rails rescue any error Handling exceptions in your API applications is quite an important thing, and if you want to keep things DRY, you should think how to do it in the proper way. In our Ruby on Rails API course, I’ve shown how to implement the error handling using ErrorSerializer and ActiveModelSerializers gem […]

Содержание

  1. Rails rescue any error
  2. The final approach
  3. More detailed errors
  4. Serializing the error in Ruby Application
  5. Rising errors in the application
  6. Handling validation errors
  7. Summary
  8. Rails API Painless Error Handling and Rendering
  9. Recognizing the Repetition in API Error Response Rendering
  10. The API
  11. API Specs
  12. Purchases
  13. Error Code Pattern
  14. API Controllers
  15. Custom Error Classes
  16. Refactoring The Controller
  17. Universal Error Rescuing and Response Rendering
  18. Conclusion
  19. Understanding Ruby and Rails: Rescuable and rescue_from
  20. This article targets Rails 2.3 Rails 3
  21. rescue_from and Rails
  22. rescue_from and Ruby
  23. A word of warning
  24. Related Posts

Rails rescue any error

Handling exceptions in your API applications is quite an important thing, and if you want to keep things DRY, you should think how to do it in the proper way. In our Ruby on Rails API course, I’ve shown how to implement the error handling using ErrorSerializer and ActiveModelSerializers gem and here I’m going to show you even better approach to this topic when you can unify EVERY error across the whole API application.

Ruby On Rails REST API
The complete guide

Create professional API applications that you can hook anything into! Learn how to code like professionals using Test Driven Development!

UPDATE: I’ve recently came with even greater way of handling Errors in Rails Web applications using «dry-monads»! It still uses this approah to serilize the errors for JSON:API purposes, but the actual mapping can be done in the more neat way!

The final approach

There is no point to cover the whole thought process of how we came with the final result, but if you’re interested in any particular part just say it in the comments. The basic assumptions were to keep things DRY and unified across the whole application.

So here is the code.

The standard error.

First of all we needed to have the Base error, which will be a fallback for any exception risen by our application. As we use JSON API in every server’s response, we wanted to always return an error in the format that JSON API describes.

We extracted all error-specific parts for every HTML status code we wanted to support, having a fallback to 500.

More detailed errors

As you can see this basic error was just a scaffold we could use to override particular attributes of the error object. Having that implemented, we were able to instantiate several case-specific errors to deliver more descriptive messages to our clients.

All errors are very clean and small, without any unnecessary logic involved. That’s reasonable as we don’t want to give them an opportunity to fail in an unexpected way, right?

Anyway defining the error objects is only the half of the job.

Serializing the error in Ruby Application

This approach above allowed us to use something like:

To serialize the standard responses we use fast_jsonapi gem from Netflix. It’s quite nice for the usual approach, but for error handling not so much so we decided to write our own ErrorSerializer.

The logic is simple. It accepts an object with status, title, detail and source methods, and creates the serialized responses in the format of:

The only problem here is that handling all of those errors in every action of the system will end up with a lot of code duplications which is not very DRY, is it? I could just raise proper errors in the services, but standard errors, like ActiveRecord::RecordNotFound would be tricky. This is then what we ended up within our API ApplicationController:

We just included the ErrorHandler module, where we implemented all mappings and the logic responsible for all error handling.

At the top, we added a nice mapper for all errors we expect to happen somewhere. Then we rescue from the default error for the rescue block, which is the StandardError, and call the handle_error method with the risen object.

Inside of this method we just do the mapping of the risen error to what we have server responses prepared to. If none of them matches, we fall back to our Errors::StandardError object so client always gets the nice error message in the server response.

We can also add extra notifiers for any error that is not mapped in the handler module, so application admins will be able to track the unexpected results.

Rising errors in the application

In Driggl we managed to create a unified solution for the whole error handling across our API application. This way we can raise our errors in a clean way without repeating any rescue blocks, and our ApplicationController will always handle that properly.

Handling validation errors

Well, that is a nice solution, but there is one thing we intentionally omitted so far and it is: validation failure.

The problem with validations is that we can’t write the error object for invalid request just as we did for the rest, because:

  • the failure message differs based on the object type and based on attributes that are invalid
  • one JSON response can have multiple errors in the returned array.

This requires us add one more error, named Invalid, which is an extended version of what we had before.

You can see that the main difference here is the serialized_hash and initialize method. The initialize method allows us to pass error messages hash into our error object, so then we can properly serialize the error for every single attribute and corresponding message.

Our ErrorSerializer should handle that out of the box, returning:

The last thing, however, is to rise it somewhere, so the handler will get the exact error data to proceed.

In the architecture we have, it’s a not big deal. It would be annoying if we would go with updating and creating objects like this:

As this would force us to rescue the ActiveRecord::RecordInvalid error in every action, and instantiate our custom error object there like this:

Which again would end up with repeating a lot of rescue blocks across the application.

In Driggl however, we do take advantage of Trailblazer architecture, with contracts and operations, which allows us to easily unify every controller action in the system.

I won’t go into details of Trailbalzer in this article, but the point is that we could handle the validation errors once inside of the process_operation! method definition and everything works like a charm across the whole app, keeping things still nice and DRY

Summary

You could think it’s a lot of code, but really, for big applications it’s just nothing comparing to repeating it in hundred of controllers and other files. In this form we managed to unify all our errors across the whole API application and we don’t need to worry anymore about unexpected failures delivering to the client.

I hope this will be useful for you too, and if you’ll find any improvements for this approach, don’t hesitate to let me know in the comments!

Источник

Rails API Painless Error Handling and Rendering

Are you sick and tired of handling endless exceptions, writing custom logic to handle bad API requests and serializing the same errors over and over?

What if I told you there was a way to abstract away messy and repetitive error raising and response rendering in your Rails API? A way for you to write just one line of code (okay, two lines of code) to catch and serialize any error your API needs to handle? All for the low low price of just $19.99!

Okay, I’m kidding about that last part. This is not an infomercial.

Although Liz Lemon makes the snuggie (sorry, «slanket») look so good, just saying.

In this post, we’ll come to recognize the repetitive nature of API error response rendering and implement an abstract pattern to DRY up our code. How? We’ll define a set of custom errors, all subclassed under the same parent and tell the code that handles fetching data for our various endpoints to raise these errors. Then, with just a few simple lines in a parent controller, we’ll rescue any instance of this family of errors, rendering a serialized version of the raised exception, thus taking any error handling logic out of our individual endpoints.

I’m so excited. Let’s get started!

Recognizing the Repetition in API Error Response Rendering

The API

For this post, we’ll imagine that we’re working on a Rails API that serves data to a client e-commerce application. Authenticated users can make requests to view their past purchases and to make a purchase, among other things.

We’ll say that we have the following endpoints:

Any robust API will of course come with specs.

API Specs

Our specs look something like this:

Purchases

Request
Success Response
Error Response
code message
3000 Can’t find purchases without start and end date

Yes, I’ve decided querying purchases requires a date range. I’m feeling picky.

Request
Success Response
Error Response
code message
4000 item_id is required to make a purchase

Error Code Pattern

With just a few endpoint specs, we can see that there is a lot of shared behavior. For the GET /purchases request and POST /purchases requests, we have two specific error scenarios. BUT, in both of the cases in which we need to respond with an error, the response format is exactly the same. It is only the content of the code and message keys of our response body that needs to change.

Let’s take a look at what this error handling could look like in our API controllers.

API Controllers

Both of our example endpoints contain error rendering logic and they are responsible for composing the error to be rendered.

This is repetitious, and will only become more so as we build additional API endpoints. Further, we’re failing to manage our error generation in a centralized away. Instead creating individual error JSON packages whenever we need them.

Let’s clean this up. We’ll start by building a set of custom error classes, all of which will inherit from the same parent.

Custom Error Classes

All of our custom error classes will be subclassed under ApiExceptions::BaseException . This base class will contain our centralized error code map. We’ll put our custom error classes in the lib/ folder.

We’ve done a few things here.

  • Inherit BaseException from StandardError , so that instances of our class can be raised and rescued.
  • Define an error map that will call on a proc to generate the correct error code and message.
  • Created attr_reader s for the attributes we want to serialize
  • Included ActiveModel::Serialization so that instances of our class can be serialized by Active Model Serializer.
  • Defined an #initialize method that will be called by all of our custom error child classes. When this method runs, each child class will use the error map to set the correct values for the @status , @code and @message variables.

Now we’ll go ahead and define our custom error classes, as mapped out in our error map.

Now that are custom error classes are defined, we’re ready to refactor our controller.

Refactoring The Controller

For this refactor, we’ll just focus on applying our new pattern to a single endpoint, since the same pattern can be applied again and again. We’ll take a look at the POST /purchases request, handled by PurchasesController#create

Instead of handling our login directly in the controller action, we’ll build a service to validate the presence of item_id . The service should raise our new custom ApiExceptions::PurchaseError::ItemNotFound if there is no item_id in the params.

Our service is kind of like a service-model hybrid. It exists to do a job for us––generate a purchase––but it also needs a validation and it will be serialized as the response body to our API request. For this reason, we’ll define it in app/models

Now, let’s build our custom validator to check for the presence of item_id and raise our error if it is not there.

This custom validator will be called with the #valid? method runs.

So, the very simple code in our Purchases Controller will raise the appropriate error if necessary, without us having to write any control flow in the controller itself.

But, you may be wondering, how will we rescue or handle this error and render the serialized error?

Universal Error Rescuing and Response Rendering

This part is really cool. With the following line in our Application Controller, we can rescue *any error subclassed under ApiExceptions::BaseException :

This line will rescue any such errors by calling on a method render_error_response , which we’ll define here in moment, and passing that method the error that was raised.

all our render_error_response method has to do and render that error as JSON.

Our ApiExceptionSerializer is super simple:

And that’s it! We’ve gained super-clean controller actions that don’t implement any control flow and a centralized error creation and serialization system.

Let’s recap before you go.

Conclusion

We recognized that, in an API, we want to follow a strong set of conventions when it comes to rendering error responses. This can lead to repetitive controller code and an endlessly growing and scattered list of error message definitions.

To eliminate these very upsetting issues, we did the following:

  • Built a family of custom error classes, all of which inherit from the same parent and are namespaced under ApiExceptions .
  • Moved our error-checking control flow logic out of the controller actions, and into a custom model.
  • Validated that model with a custom validator that raises the appropriate custom error instance when necessary.
  • Taught our Application Controller to rescue any exceptions that inherit from ApiExceptions::BaseException by rendering as JSON the raised error, with the help of our custom ApiExceptionSerializer .

Keep in mind that the particular approach of designing a custom model with a custom validator to raise our custom error is flexible. The universally applicable part of this pattern is that we can build services to raise necessary errors and call on these services in our controller actions, thus keeping error handling and raising log out of individual controller actions entirely.

Источник

Understanding Ruby and Rails: Rescuable and rescue_from

This article targets Rails 2.3 Rails 3

The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

Last time I talked about the ActiveSupport Module#delegate method. Today, I want to introduce an other poweful ActiveSupport module: Rescuable , also known in the Rails ecosystem as rescue_from .

rescue_from and Rails

Starting from the release 2.0, Rails provides a clean way to rescue exceptions in a controller, mapping specific error classes to corresponding handlers.

Let’s see an example. A call to ActiveRecord#find raises an ActiveRecord::RecordNotFound exception when the record passed as parameter doesn’t exist. Assuming you want to display a nice 404 error page, you need to rescue the exception in each action where a find call is performed.

As you can see, this approach leads to lot of code duplication if you count the number of find calls for each action per model. The rescue_from method is exactly the solution we are looking for. Instead of catching the exception at action-level, we instruct the controller to rescue all the ActiveRecord::RecordNotFound errors and forward the exception to the proper handler.

The rescue_from method also accepts a block or a Proc . And if you need, you can also selectively rescue exceptions according to the error message or other properties.

rescue_from and Ruby

The rescue_from was born as a Rails feature but because it’s packaged in the ActiveSupport::Rescuable module, you can easily reuse it elsewhere in your code to take advantage of the same clean and concise exception handling mechanism.

All you have to do is to require ActiveSupport library and include the ActiveSupport::Rescuable module in your class. If you are in a Rails project, ActiveSupport is already loaded. Then, add a rescue block and use the rescue_with_handler method to filter any error raised by the application.

The following is a simplified example extracted from RoboDomain. The Queue::Jobs::Base is the base class for all DelayedJob jobs. Each child class implements the perform method, as requested by DelayedJob . However, the base class provides an internal method called execute which wraps all executions and rescues from some known errors to prevent DelayedJob to re-schedule the failed task.

As you can see, using the ActiveSupport::Rescuable module I don’t need to clutter my code with multiple begin / rescue / raise statements.

A word of warning

Like any reusable pattern, ActiveSupport::Rescuable is not the ultimate and definitive solution for any piece of code where you need to rescue from an exception. Use it if you actually need it, don’t try to force your code to fit this implementation.

  • 07 Jan 2018 The Art of Invisibility book
  • 27 Dec 2016 How I use StackOverflow
  • 08 Nov 2016 9 years of 1Password

Filed under: Programming — About: Ruby Ruby on Rails activesupport exceptions

Источник

Exceptions in Ruby

In Ruby, Exceptions are created using raise command:

1begin
2  raise "boom"
3end

If we execute that code then we will see the error:


Notice that Ruby is saying that it is a RuntimeError. Here is Ruby’s official
documentation about
RuntimeError. Above code
can also be written like this:

1raise RuntimeError, "boom"

As per Ruby’s documentation if when we do not mention any class while raising an
exception then by default it is RuntimeError class.

In Ruby all exceptions are subclasses of Exception class.

Hierarchy of Ruby Exception class

Ruby has lots of built in exceptions. Here is hierarchy of all Ruby’s
exceptions:

1Exception
2  NoMemoryError
3  ScriptError
4    LoadError
5    NotImplementedError
6    SyntaxError
7  SecurityError
8  SignalException
9    Interrupt
10  StandardError
11    ArgumentError
12      UncaughtThrowError
13    EncodingError
14    FiberError
15    IOError
16      EOFError
17    IndexError
18      KeyError
19      StopIteration
20    LocalJumpError
21    NameError
22      NoMethodError
23    RangeError
24      FloatDomainError
25    RegexpError
26    RuntimeError
27    SystemCallError
28      Errno::*
29    ThreadError
30    TypeError
31    ZeroDivisionError
32  SystemExit
33  SystemStackError

The rescue method catches a class and all its subclasses

1begin
2  do_something
3rescue NameError
4end

Here we are rescuing all NameError exceptions. However NoMethodError will
also be rescued because NoMethodError is a subclass of NameError.

For example, consider a api_exceptions.rb file where we are rescuing from all
the exceptions like so:

1module ApiExceptions
2  rescue_from StandardError, with: :handle_api_exception
3  rescue_from ActiveRecord::RecordNotUnique, with: :handle_record_not_unique_exception
4
5  def handle_api_exception(exception)
6    case exception
7    # handles the exceptions
8  end
9
10  def handle_record_not_unique_exception(exception)
11    # handles record not unique exception
12  end
13end

The RecordNotUnique exception is a child class of StandardError. When we
rescue from StandardError all the child exception classes will also be caught.
So the rescue_from ActiveRecord::RecordNotUnique in the above example is
redundant. The code will never reach this line because the RecordNotUnique
exception will already be caught by the rescue_from StandardError statement.

The correct way of rescuing the errors will be, like so:

1module ApiExceptions
2  rescue_from StandardError, with: :handle_api_exception
3
4  def handle_api_exception(exception)
5    case exception
6
7    when ActiveRecord::RecordNotFound
8      handle_record_not_unique_exception(exception)
9
10    # handles the other exceptions
11  end
12
13  def handle_record_not_unique_exception(exception)
14    # handles record not unique exception
15  end
16end

In the above example, we are rescuing StandardError and all its children
classes with handle_api_exception method. And inside handle_api_exception
method case is used to handle
the required child exception classes specifically.

Certain exceptions and when they are raised

  • Pundit::NotAuthorizedError is raised when any authorization error from
    Pundit gem raises and we
    can handle it with 403 or forbidden status.

  • ActionController::ParameterMissing is raised when a required parameter is
    missing. Consider a case when we have strong params defined for a post
    request and those params are missing in the request header. We can handle this
    via 500 or internal_server_error status.

  • ActiveRecord::RecordNotFound is raised when Rails does not find any
    record. When we use find method to search for a record with the provided
    id in params and the id is mistaken or the record is missing from DB,
    Rails raises ActiveRecord::RecordNotFound exception. We can handle this
    with a 404 or not_found status.

  • ActiveRecord::RecordInvalid is raised on failure of validations declared in
    model for any record creation or updation. Let’s say we have some email
    validation declared in model and the input email does not match with the given
    Regex pattern Rails raises ActiveRecord::RecordInvalid exception. We can
    handle this with a 422 or unprocessable_entity status.

  • ActiveRecord::RecordNotUnique is raised when a record cannot be inserted or
    updated because it would violate a uniqueness constraint from DB. We can
    handle this with unprocessable_entity or 422 status.

  • PG::NotNullViolation: ERROR: null value in column «name» of relation «users» violates not-null constraint
    for missing mandatory input. We should handle the database errors starting
    with PG:: or SQLite3:: with internal_server_error or 500 status. If we
    keep this as unprocessable_entity then it won’t raise Honeybadger issues,
    meaning it will log the error silently. These errors are very rare and we
    should be getting notified of these errors by Honeybadger in Github.

Raising error using class

Following two lines do the same thing:

1raise "boom"
2raise RuntimeError, "boom"

We can raise exceptions of a particular class by stating the name of that
exception class:

1raise ArgumentError, "two arguments are needed"
2raise LoadError, "file not found"

Default rescue is StandardError

rescue without any argument is same as rescuing StandardError:


Above statement is same as the one given below:

1begin
2rescue StandardError
3end

Catching multiple types of exceptions in one shot

We can catch multiple types of exceptions in one statement:

1begin
2rescue ArgumentError,NameError
3end

Catching exception in a variable

We can catch exception in a variable like this:

1begin
2rescue StandardError => e
3end

Here e is an exception object. The three main things we like to get from an
exception object are «class name», «message» and «backtrace».

Let’s print all the three values:

1begin
2   raise "boom"
3rescue StandardError => e
4  puts "Exception class is #{e.class.name}"
5  puts "Exception message is #{e.message}"
6  puts "Exception backtrace is #{ e.backtrace}"
7end

Custom exceptions

Sometimes we need custom exceptions. Creating custom exceptions is easy:

1class NotAuthorizedError < StandardError
2end
3
4raise NotAuthorizedError.new("You are not authorized to edit record")

NotAuthorizedError is a regular Ruby class. We can add more attributes to it
if we want:

1class NotAuthorizedError < StandardError
2  attr_reader :account_id
3
4  def initialize(message, account_id)
5    #invoke the constructor of parent to set the message
6    super(message)
7
8    @account_id = account_id
9  end
10end
11
12raise NotAuthorizedError.new("Not authorized", 171)

rescue nil

Sometimes we see code like this:


The above code is equivalent to the following code:

1begin
2  do_something
3rescue
4  nil
5end

The above code can also be written like so, since by default StandardError is
raised:

1begin
2  do_something
3rescue StandardError
4  nil
5end

Exception handling in Ruby on Rails using rescue_from

A typical controller could look like this:

1class ArticlesController < ApplicationController
2  def show
3    @article = Article.find(params[:id])
4  rescue ActiveRecord::RecordNotFound
5    render_404
6  end
7
8  def edit
9    @article = Article.find(params[:id])
10  rescue ActiveRecord::RecordNotFound
11    render_404
12  end
13end

We can use
rescue_from
to catch the exception.

The rescue_from directive is an exception handler that rescues the specified
exceptions raised within controller actions and reacts to those exceptions with
a defined method.

For example, the following controller rescues ActiveRecord::RecordNotFound
exceptions and passes them to the render_404 method:

1class ApplicationController < ActionController::Base
2  rescue_from ActiveRecord::RecordNotFound, with: :render_404
3
4  def render_404
5  end
6end
7
8class ArticlesController < ApplicationController
9  def show
10    @article = Article.find(params[:id])
11  end
12
13  def edit
14    @article = Article.find(params[:id])
15  end
16end

The advantage to rescue_from is that it abstracts the exception handling away
from individual controller actions, and instead makes exception handling a
requirement of the controller.

The rescue_from directive not only makes exception handling within controllers
more readable, but also more regimented.

Rescuing from specific exception

Ruby’s Exception is the parent class to all errors. So one might be tempted to
always rescue from this exception class and get the «job» done. But DON’T!

Exception includes the class of errors that can occur outside your
application. Things like memory errors, or SignalException::Interrupt(sent
when you manually quit your application by hitting Control-C), etc. These are
the errors that you don’t want to catch in your application as they are
generally serious and related to external factors. Rescuing the Exception
class can cause very unexpected behaviour.

StandardError is the parent of most Ruby and Rails errors. If you catch
StandardError you’re not introducing the problems of rescuing Exception, but
it is not a great idea either. Rescuing all application-level errors might cover
up unrelated bugs that you don’t know about.

The safest approach is to rescue the error(or errors) you are expecting and deal
with the consequences of that error inside the rescue block.

In the event of an unexpected error in your application you want to know that a
new error has occurred and deal with the consequences of that new error inside
its own rescue block.

Being specific with rescue means your code doesn’t accidentally swallow new
errors. You avoid subtle hidden errors that lead to unexpected behaviour for
your users and bug hunting for you.

Do not use exception as control flow

Let’s look at the following code:

1class QuizController < ApplicationController
2  def load_quiz
3    @quiz = current_user.quizzes.find(params[:id])
4  rescue ActiveRecord::RecordNotFound
5    format.json { render status: :not_found, json: { error: "Quiz not found"}}
6  end
7end

In the above code when quiz id is not found then an exception is raised and then
that exception is immediately caught.

Here the code is using exception as a control flow mechanism. What it means is
that the code is aware that such an exception could be raised and is prepared to
deal with it.

The another way to deal with such a situation would be to not raise the
exception in the first place. Here is an alternative version where code will not
be raising any exception:

1class QuizController < ApplicationController
2  def load_quiz
3    @quiz = current_user.quizzes.find_by_id(params[:id])
4    unless @quiz
5      format.json { render status: :not_found, json: { error: "Quiz not found"}}
6    end
7  end
8end

In the above case instead of using find code is using find_by_id which would
not raise an exception in case the quiz id is not found.

In Ruby world we like to say that an exception should be an exceptional
case
. Exceptional case could be database is down or there is some network
error. Exception can happen anytime but in this case code is not using catching
an exception as a control flow.

Long time ago in the software engineering world GOTO was used a lot. Later
Edsger W. Dijkstra wrote a
famous letter
Go To Statement Considered Harmful.
Today it is a well established that using GOTO is indeed harmful.

Many consider using Exception as a control flow similar to using GOTO since when
an exception is raised it breaks all design pattern and exception starts flowing
through the stack. The first one to capture the exception gets the control of
the software. This is very close to how GOTO works. In Ruby world it is well
established practice to
not to use Exception as a control flow.

Using bang methods in controller actions

But, just like everything in software engineering, the suggestion we had made in
last section also has some exceptions. Like in controllers etc, we should be
trying our level best to keep controllers as skinny as possible. So adding
repetitive unless or if statements as a replacement for rescue statements
won’t scale. Thus in such cases, what we should do is use the bang(!) versions
of ActiveRecord methods, like create!, update!, destroy! or save! within
the controller actions. This would raise an exception in case there’s a failure.

But where to handle theses exceptions? Well, in the chapter where we had cleaned
up the application controller, we had added a concern named ApiExceptions.
This concern is included in the ApplicationController. Which means all other
controllers will be having access to the methods defined in that concern. In
that concern we have several rescue_from statements, which handles specific
exceptions.

Thus in our controller, we could write something like so, and both the success
and failure cases will be handled:

1class QuizController < ApplicationController
2  before_action :load_quiz!, only: %i[update]
3
4  def update
5    @quiz.update!(quiz_params) # control wouldn't even reach here if exception was raised in load_quiz!
6    respond_with_success(t("successfully_created", entity: "Quiz")) # control will only reach here if the above statement didn't raise any exception
7  end
8
9  private
10
11    def load_quiz!
12      @quiz = current_user.quizzes.find_by!(id: params[:id])
13    end
14end

Notice how we have named the method as load_quiz! with a bang(!) rather than
simply load_quiz? It’s to denote that the particular method has the potential
to raise an exception. If no record is found by the find_by! method then a
ActiveRecord::RecordNotFound exception will be raised.

There is a problem in the above code block, which has to deal with code
conventions that we follow in BigBinary. That’s, if we are querying using the
id attribute only then we should use the find method over find_by! because
in the find method we can directly pass the id value without defining any
key and it makes the code cleaner. If for a given id no record is found by
find method then the same exception will be raised that is
ActiveRecord::RecordNotFound.

So, in the above code block the load_quiz! method needs to be updated like so:

1def load_quiz!
2  @quiz = current_user.quizzes.find(id)
3end

There is nothing to commit in this chapter since all we had done was
learning the basics of exception handling in Ruby.

Active Record provides many mighty tools to streamline the work of building a backend API, including for when things go wrong. In this post, we’ll walk through three error-handling techniques, getting more sophisticated and powerful as we go.

For the sake of simplicity, our examples will deal with failing to find a record in our database, which we’ll illustrate in the context of a standard, RESTful show controller action. However, these techniques are applicable to other types of errors as well.

Basic Error-Handling with Conditional Logic

The simplest way to handle Active Record errors is just to use conditional logic in your controller actions to specify what your API should do if an error arises. We might write our show action like this:

def show
  dingus = Dingus.find_by(id: params[:id])
  if dingus?
    render json: dingus, status: :ok
  else
    render json: {error: "Dingus not found"}, status: :not_found
  end
end

Enter fullscreen mode

Exit fullscreen mode

Let’s break down what’s happening here. First, we initialize a variable to refer to the Dingus record we want from the database, and we use find_by to retrieve it with the id provided by the params hash. (Read more about the params hash here.) Then, the conditional statement if dingus? checks whether that variable has a truthy or falsey value. This works because if the find_by operation locates a record and assigns it to the variable, dingus is truthy. If no record is found, the value of dingus is nil, which is falsey. Accordingly, if the condition is met, the API sends the data in its response, along with the successful :ok status code. Otherwise, it sends an error message, which we’ve produced manually as a simple hash, along with the appropriate :not_found status.

Handling errors like this has certain advantages. For one thing, this approach is easy to understand, and all the code for it exists in one place. It gives you very specific control over how an individual controller action responds to a certain error, and for that reason this technique can sometimes be useful even if you’re using other more sophisticated methods elsewhere—particularly if you’re creating custom controller actions.

However, for common errors, this technique is inefficient and clunky. You don’t want to have to write separate if/else logic everywhere, and you definitely shouldn’t compose your own error messages for every single case.

Basic Error-Handling with Rescue Blocks

Fortunately, Active Record gives us more tools and a better way to do things. In our first example, we used a simple logical condition to check whether or not our show action found a record, but we can take advantage of some built-in functionality instead.

Certain Active Record methods return instances of special classes called exceptions in the event of failure. For instance, the find method takes an id and returns a RecordNotFound exception if there’s no record with a matching id attribute. It is less flexible than the find_by method in our first example, which can search using whatever attribute we like, but returning an exception instead of just nil when it doesn’t find a matching record is a major advantage. For one thing, exceptions come with their own error messages, which we can access and send to the frontend rather than having to compose them ourselves. More importantly, they also enable us to use rescue blocks to define how we want to handle errors.

The rescue keyword is kind of like special conditional logic that looks out for exceptions of the specified type. When the right kind of exception occurs, the code block runs, doing whatever we’ve written to deal with the error.

Let’s rewrite our show action to use a rescue block:

def show
  render json: Dingus.find(params[:id]), status: :ok
rescue ActiveRecord::RecordNotFound
  render json: {error: "Dingus not found"}, status: :not_found
end

Enter fullscreen mode

Exit fullscreen mode

Now we’re using find to retrieve the requested record, and simply sending it to the frontend without assigning it to a variable first (though you can certainly still do so, and might have reason to in certain cases). If no record exists with a matching id, find returns an ActiveRecord::RecordNotFound exception—and when that happens, our rescue block kicks in and runs the same code we used in the else part of our first example.

This is a step in the right direction, but we still have to write a rescue block for each controller action. Maybe that’s fine if we want to deal with a certain kind of exception for a single action, but it’s annoying if we want to handle the same kinds of errors in multiple places.

Intermediate Error-Handling with rescue_from

Fortunately, we can entirely separate our error-handling from our controller actions. Just like rescue, rescue_from defines a response to a specific type of exception. However, rather than being attached to a particular action, a rescue_from is independently defined, and applies any time the specified exception occurs, no matter what action it comes from.

With a rescue_from, our whole example controller now looks like this:

class DingusesController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

  def show
    render json: Dingus.find(params[:id]), status: :ok
  end

  private
  def render_not_found
    render json: {error: "Dingus not found"}, status: :not_found
  end
end

Enter fullscreen mode

Exit fullscreen mode

Now the show action doesn’t need to include anything concerned with potential errors. We’ve refactored the error response into a separate method called render_not_found, which our rescue_from will use whenever a RecordNotFound exception occurs.

At this point, since our error-handling is separated from our controller actions, we can make it even more generally performant by relocating it to our top-level controller, like so:

class ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

  private
  def render_not_found(exception)
    render json: {exception.model: "Not found"}, status: :not_found
  end
end

Enter fullscreen mode

Exit fullscreen mode

Since DingusesController inherits from ApplicationController, it will use the rescue_from defined there—as will all our other controllers. We’ve also adjusted the private render_not_found method in order to generalize and reuse our error response whenever a RecordNotFound exception occurs anywhere. Now it takes the exception instance as a parameter so we can call exception.model to get whatever kind of resource wasn’t found. In our example case, this would give us {Dingus: "Not found"}.

TLDR

The best approach for handling most Active Record errors in to use rescue_from in your top-level application controller. It’s a bit more abstract, but it’s the most effective and efficient way to cover common types of errors for multiple database resources. Once you understand how to use rescue_from, there’s probably little reason to ever use an individual rescue block.

However, there may be still be times where good old if/else logic can provide a good one-off solution to deal with errors in special cases.

This article targets Rails 2.3 Rails 3

The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

Last time I talked about the ActiveSupport Module#delegate method. Today, I want to introduce an other poweful ActiveSupport module: Rescuable, also known in the Rails ecosystem as rescue_from.

Starting from the release 2.0, Rails provides a clean way to rescue exceptions in a controller, mapping specific error classes to corresponding handlers.

Let’s see an example. A call to ActiveRecord#find raises an ActiveRecord::RecordNotFound exception when the record passed as parameter doesn’t exist. Assuming you want to display a nice 404 error page, you need to rescue the exception in each action where a find call is performed.

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def edit
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  rescue ActiveRecord::RecordNotFound
    render_404
  end
end

class UserController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  # ...
end

As you can see, this approach leads to lot of code duplication if you count the number of find calls for each action per model. The rescue_from method is exactly the solution we are looking for. Instead of catching the exception at action-level, we instruct the controller to rescue all the ActiveRecord::RecordNotFound errors and forward the exception to the proper handler.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, :with => :render_404
end

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end

  def edit
    @post = Post.find(params[:id])
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  end

end

class UserController < ApplicationController
  def show
    @user = User.find(params[:id])
  end

  # ...
end

The rescue_from method also accepts a block or a Proc. And if you need, you can also selectively rescue exceptions according to the error message or other properties.

rescue_from and Ruby

The rescue_from was born as a Rails feature but because it’s packaged in the ActiveSupport::Rescuable module, you can easily reuse it elsewhere in your code to take advantage of the same clean and concise exception handling mechanism.

All you have to do is to require ActiveSupport library and include the ActiveSupport::Rescuable module in your class. If you are in a Rails project, ActiveSupport is already loaded. Then, add a rescue block and use the rescue_with_handler method to filter any error raised by the application.

class MyClass
  include ActiveSupport::Rescuable

  def method
    # ...
  rescue Exception => exception
    rescue_with_handler(exception) || raise
  end
end

The following is a simplified example extracted from RoboDomain. The Queue::Jobs::Base is the base class for all DelayedJob jobs. Each child class implements the perform method, as requested by DelayedJob. However, the base class provides an internal method called execute which wraps all executions and rescues from some known errors to prevent DelayedJob to re-schedule the failed task.

class Queue::Jobs::Base
  include ActiveSupport::Rescuable

  rescue_from ActiveRecord::RecordNotFound, :with => :known_error

  protected

    def execute(&block)
      yield
    rescue Exception => exception
      rescue_with_handler(exception) || raise
    end

    def known_error(exception)
      @error = exception
      Rails.logger.error "[JOBS] Exception #{exception.class}: #{exception.message}"
    end

end

class Queue::Jobs::FetchWhois < Queue::Jobs::Base

  rescue_from Hostname::NotLikeDomain, :with => :known_error
  # ActiveRecord::RecordNotFound already defined in parent class

  def initialize(hostname_id)
    @hostname_id = hostname_id
  end

  def perform
    execute do
      hostname = Hostname.find(@hostname_id)
    end
  end

end

As you can see, using the ActiveSupport::Rescuable module I don’t need to clutter my code with multiple begin/rescue/raise statements.

A word of warning

Like any reusable pattern, ActiveSupport::Rescuable is not the ultimate and definitive solution for any piece of code where you need to rescue from an exception. Use it if you actually need it, don’t try to force your code to fit this implementation.

По этому руководству вы изучите, как работают контроллеры, и как они вписываются в цикл запроса к вашему приложению.

После его прочтения, вы узнаете:

  • Как следить за ходом запроса через контроллер.
  • Как ограничить параметры, переданные в контроллер.
  • Как и зачем хранятся данные в сессии или куки.
  • Как работать с фильтрами для выполнения кода в течение обработки запроса.
  • Как использовать встроенную в Action Controller HTTP аутентификацию.
  • Как направлять потоковые данные прямо в браузер пользователя.
  • Как отфильтровывать чувствительные параметры, чтобы они не появлялись в логах приложения.
  • Как работать с исключениями, которые могут порождаться в течение обработки запроса.

Action Controller это C в аббревиатуре MVC. После того, как роутер определит, какой контроллер использовать для обработки запроса, контроллер ответственен за осмысление запроса и генерацию подходящего ответа. К счастью, Action Controller делает за вас большую часть грязной работы и использует элегантные соглашения, чтобы сделать это по возможности максимально просто.

Для большинства приложений, основанных на RESTful, контроллер получает запрос (это невидимо для вас, как для разработчика), извлекает или сохраняет данные в модели и использует вью для создания результирующего HTML. Если контроллеру необходимо работать немного по-другому, не проблема, это всего лишь наиболее распространенный способ работы контроллера.

Таким образом, контроллер можно рассматривать как посредника между моделями и вью. Он делает данные модели доступными вью, так что она может отображать эти данные пользователю, и он сохраняет или обновляет данные от пользователя в модель.

Более детально о процессе маршрутизации смотрите Роутинг в Rails.

Соглашение по именованию контроллеров в Rails устанавливает предпочтение множественного числа в последнем слове имени контроллера, хотя строго это не требуется (например, ApplicationController). К примеру, ClientsController более предпочтителен, чем ClientController, SiteAdminsController более предпочтителен, чем SiteAdminController или SitesAdminsController, и так далее.

Следование этому соглашению позволяет вам использовать генераторы маршрутов по умолчанию (например, resources и т.п.) без необходимости определять каждый :path или :controller, и сохраняет последовательным использование хелперов именованных путей во всем вашем приложении. Подробнее смотрите в руководстве Макеты и рендеринг в Rails.

Соглашение по именованию контроллеров отличается от соглашения по именованию моделей, которые, как ожидается, будут именоваться в единственном числе.

Контроллер — это класс Ruby, унаследованный от ApplicationController и содержащий методы, как и любой другой класс. Когда ваше приложение получает запрос, роутинг определяет, какой контроллер и экшн нужно запустить, затем Rails создает экземпляр этого контроллера и запускает метод с именем, как у экшна.

class ClientsController < ApplicationController
  def new
  end
end

В качестве примера, если пользователь перейдет на /clients/new в приложении, чтобы добавить нового клиента, Rails создаст экземпляр ClientsController и вызовет метод new. Отметьте, что пустой метод из вышеприведенного примера будет прекрасно работать, так как Rails по умолчанию отрендерит вью new.html.erb, если в экшне не будет указано иное. При создании нового Client, метод new может сделать переменную экземпляра @client доступной во вью:

def new
  @client = Client.new
end

Руководство Макеты и рендеринг в Rails объясняет это более детально.

ApplicationController унаследован от ActionController::Base, который определяет несколько полезных методов. Это руководство раскроет часть из них, но если вы любопытны, можете увидеть их все в документации по API.

Только public методы могут быть вызваны как экшны. Хорошей практикой является уменьшение области видимости методов (при помощи private или protected), не предназначенных быть экшнами, таких как вспомогательные методы и фильтры.

Некоторые имена методов зарезервированы в Controller. Случайное переопределение их как экшны, или даже как вспомогательные методы, может привести к SystemStackError. Если вы ограничиваете свои контроллеры иметь только RESTful экшны в [Ресурсном роутинге][], об этом можно не беспокоиться.

Если вы должны использовать зарезервированные методы в качестве имени экшна, это можно обойти, используя пользовательский маршрут, для направления зарезервированного имени метода на не зарезервированный метод экшна.

Возможно, вы хотите получить доступ к данным, посланным пользователем, или к другим параметрам в экшнах вашего контроллера. Существует два типа параметров, доступных в веб-приложениях. Первый — это параметры, посланные как часть URL, называемые параметрами строки запроса. Строка запроса всегда следует после «?» в URL. Второй тип параметров обычно упоминается как данные POST. Эта информация обычно приходит из формы HTML, заполняемой пользователем. Эти параметры еще называют данными POST, так как могут быть посланы только как часть HTTP-запроса метода POST. Rails не делает каких-либо различий между строковыми параметрами и параметрами POST, и они оба доступны в хэше params в вашем контроллере:

class ClientsController < ApplicationController
  # Этот экшн использует параметры строки запроса, потому что он
  # запускается HTTP-запросом метода GET, но это не влияет на
  # то, как можно получить доступ к ним.
  # URL для этого экшна выглядит как этот, запрашивающий список
  # активированных клиентов: /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # Этот экшн использует параметры POST. Они, скорее всего, пришли от
  # формы HTML, которую подтвердил пользователь. URL для этого
  # RESTful запроса будет "/clients", и данные будут посланы
  # как часть тела запроса.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # Эта строчка переопределяет поведение рендеринга по умолчанию,
      # который отрендерил бы вью "create".
      render "new"
    end
  end
end

Хэш params не ограничен одномерными ключами и значениями. Он может содержать вложенные массивы и хэши. Чтобы послать массив значений, добавьте пустую пару квадратных скобок «[]» к имени ключа:

GET /clients?ids[]=1&ids[]=2&ids[]=3

Фактический URL в этом примере будет перекодирован как «/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5b=3», так как «[» и «]» недопустимы в URL. В основном, вам не придется беспокоиться об этом, так как браузер позаботится об этом за вас, а Rails декодирует это обратно, когда получит, но если вы когда-нибудь будете отправлять эти запросы вручную, имейте это в виду.

Значение params[:ids] теперь будет ["1", "2", "3"]. Отметьте, что значения параметра всегда строковое; Rails не пытается угадать или предсказать тип.

Значения, такие как [nil] или [nil, nil, ...] в params по умолчанию заменяются на [] по причине безопасности. Подробнее смотрите в руководстве Безопасность приложений на Rails.

Чтобы послать хэш, следует заключить имя ключа в скобки:

<form accept-charset="UTF-8" action="/clients" method="post">
  <input type="text" name="client[name]" value="Acme" />
  <input type="text" name="client[phone]" value="12345" />
  <input type="text" name="client[address][postcode]" value="12345" />
  <input type="text" name="client[address][city]" value="Carrot City" />
</form>

Когда эта форма будет подтверждена, значение params[:client] будет { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }. Обратите внимание на вложенный хэш в params[:client][:address].

Объект params ведет себя как хэш, но позволяет взаимозаменяемо использовать символы и строки как ключи.

Если вы пишете приложение веб-сервиса, возможно вам более комфортно принимать параметры в формате JSON. Если заголовок «Content-Type» вашего запроса установлен в «application/json», Rails автоматически загружает ваши параметры в хэш params, к которому можно получить доступ обычным образом.

Так, к примеру, если вы пошлете такое содержимое JSON:

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

Ваш контроллер будет получать params[:company] как { "name" => "acme", "address" => "123 Carrot Street" }.

Также, если включите config.wrap_parameters в своем инициализаторе или вызовете wrap_parameters в своем контроллере, можно безопасно опустить корневой элемент в параметре JSON. Параметры будут клонированы и обернуты в ключ, соответствующий по умолчанию имени вашего контроллера. Таким образом, вышеупомянутый запрос JSON может быть записан как:

{ "name": "acme", "address": "123 Carrot Street" }

И предположим, что мы посылаем данные в CompaniesController, тогда он будет обернут в ключ :company следующим образом:

{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }

Вы сможете настроить имя ключа или определенные параметры, которые вы хотите обернуть, ознакомившись с документацией по API.

Поддержка парсинга параметров XML была извлечена в гем actionpack-xml_parser.

Хэш params будет всегда содержать ключи :controller и :action, но следует использовать методы controller_name и action_name вместо них для доступа к этим значениям. Любой другой параметр, определенный роутингом, такой как :id, также будет доступен. Например, рассмотрим перечень клиентов, где список может быть показан либо для активных, либо для неактивных клиентов. Мы можем добавить маршрут, перехватывающий параметр :status в «красивом» URL:

get '/clients/:status', to: 'clients#index', foo: 'bar'

В этом случае, когда пользователь откроет URL /clients/active, params[:status] будет установлен в «active». Когда использован этот маршрут, params[:foo] также будет установлен в «bar», как будто он был передан в строке запроса. Ваш контроллер также получит params[:action] как «index» и params[:controller] как «clients».

Можно установить глобальные параметры по умолчанию для генерации URL, определив в контроллере метод по имени default_url_options. Этот метод должен возвращать хэш с желаемыми значениями по умолчанию, ключи которого должны быть символами:

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

Эти опции будут использованы как начальная точка при генерации URL, поэтому они могут быть переопределены опциями, переданными в url_for.

Если определить default_url_options в ApplicationController, как это показано в вышеприведенном примере, эти значения по умолчанию будут использованы для генерации всех URL. Этот метод также может быть определен в одном отдельном контроллере, и в этом случае он влияет только на URL, сгенерированные в нем.

В данном запросе, на самом деле, метод не вызывается для каждого сгенерированного URL; для повышения производительности, возвращаемый хэш кэшируется, и метод выполняется не более одного раза за запрос.

С помощью сильных параметров (strong parameters) параметры Action Controller запрещены к использованию в массовых назначениях Active Model до тех пор, пока они не разрешены. Это означает, что нужно будет принять осознанное решение о том, какие атрибуты будут разрешены для массового обновления. Это лучший способ предотвратить случайную уязвимость, позволяющую пользователям обновлять чувствительные атрибуты модели.

Кроме того, параметры могут быть помечены как обязательные и будут проходить через предопределенные raise/rescue, что приведет к 400 Bad Request, если не будут переданы все обязательные параметры.

class PeopleController < ActionController::Base
  # Это вызовет исключение ActiveModel::ForbiddenAttributesError,
  # так как используется массовое назначение без явного шага permit.
  def create
    Person.create(params[:person])
  end

  # Это будет выполняться должным образом, пока в параметрах есть ключ person, иначе будет
  # вызвано исключение ActionController::ParameterMissing, которое будет
  # поймано в ActionController::Base и превращено в ошибку 400 Bad Request.
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # Использование приватного метода для инкапсуляции разрешенных параметров -
    # это всего лишь хороший паттерн, с помощью которого можно повторно
    # использовать тот же самый список разрешений при создании и обновлении.
    # Этот метод также можно адаптировать к проверке разрешенных атрибутов для
    # каждого пользователя.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end

Вызов permit подобный:

разрешает указанный ключ (:id) для включения, если он появится в params и будет иметь разрешенное скалярное значение. В ином случае ключ будет отфильтрован, таким образом, массивы, хэши и любые другие объекты не смогут быть переданы.

Разрешенные скалярные типы следующие String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile и Rack::Test::UploadedFile.

Чтобы объявить, что значение в params должно быть массивом разрешенных скалярных значений, свяжите ключ с пустым массивом:

Иногда невозможно или неудобно объявлять валидные ключи параметров хэша или его внутреннюю структуру. Просто укажите пустой хэш:

params.permit(preferences: {})

но будьте осторожны, так это открывает возможность произвольного ввода. В этом случае permit гарантирует, что значения в возвращаемой структуре являются разрешенными скалярными величинами и отфильтровывает все иное.

Чтобы разрешить полный хэш параметров, можно использовать метод permit!:

params.require(:log_entry).permit!

Это помечает хэш параметров :log_entry и любые вложенные хэши как разрешенные, и не проверяет разрешенные скалярные величины, принимается все. Следует соблюдать предельную осторожность при использовании permit!, так как он позволит массовое назначение всех текущих и будущих атрибутов модели.

Также можно использовать permit c вложенными параметрами, например:

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

Это объявление разрешает атрибуты name, emails и friends. Ожидается, что emails будет массивом разрешенных скалярных значений, и что friends будет массивом ресурсов с определенными атрибутами: у них будет атрибут name (допустимо любое скалярное значение), атрибут hobbies как массив разрешенных скалярных значений, и атрибут family, который может иметь только name (также допустимо любое скалярное значение).

Возможно вы захотите использовать разрешенные атрибуты в экшне new. В этой связи возникает проблема, из-за которой нельзя использовать require на корневом ключе, так как обычно он не существует при вызове new:

# используя `fetch`, можно предоставить значение по умолчанию и использовать
# далее Strong Parameters API.
params.fetch(:blog, {}).permit(:title, :author)

Метод класса модели accepts_nested_attributes_for позволяет обновлять и удалять связанные записи. Он основывается на параметрах id и _destroy:

# permit :id и :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

Хэши с числовыми ключами трактуются по-другому, и можно объявить атрибуты так, как будто они являются прямыми детьми. Такой тип параметров можно получить при использовании accepts_nested_attributes_for в сочетании со связью has_many:

# Чтобы разрешить следующие данные:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}

params.require(:book).permit(:title, chapters_attributes: [:title])

Рассмотрим ситуацию, когда есть параметры, представляющие имя продукта, и хэш произвольных данных, связанных с этим продуктом, и нужно разрешить атрибут name продукта, а также весь хэш данных:

def product_params
  params.require(:product).permit(:name, data: {})
end

Strong parameter API был разработан для наиболее общих вариантов использования. Это не панацея от всех ваших проблем белого списка. Однако можно легко смешивать API с вашим собственным кодом для адаптации к вашей ситуации.

В приложении есть сессия для каждого пользователя, в которой можно хранить небольшие объемы данных, которые будут персистентными между запросами. Сессия доступна только в контроллере и во вью, и может использовать один из нескольких механизмов хранения:

  • ActionDispatch::Session::CookieStore — Хранит все на клиенте.
  • ActionDispatch::Session::CacheStore — Хранит данные в кэше Rails.
  • ActionDispatch::Session::MemCacheStore — Хранит данные в кластере memcached (эта устаревшая реализация, вместо нее рассмотрите использование CacheStore).
  • ActionDispatch::Session::ActiveRecordStore — Хранит данные в базе данных с использованием Active Record. (требует гем activerecord-session_store).
  • Пользовательское хранилище или хранилище, предоставленное сторонним гемом

Все хранилища сессии используют куки для хранения уникального ID каждой сессии (вы должны использовать куки, Rails не позволяет передавать ID сессии в URL, так как это менее безопасно).

В большинстве хранилищ этот ID используется для поиска данных сессии на сервере, в т.ч. в таблице базы данных. Имеется одно исключение, это дефолтное и рекомендуемое хранилище сессии — CookieStore — которое хранит все данные сессии в куки (ID остается по-прежнему доступным, если он нужен). Преимущества его заключаются в легкости, отсутствии необходимости настройки для нового приложения чтобы использовать сессию. Данные в куки криптографически подписаны, что делает их защищенными от взлома. И они также зашифрованы, таким образом любой получивший к ним доступ, не сможет прочитать их содержимое (Rails не примет их, если они были отредактированы).

CookieStore могут хранить около 4 Кбайт данных — намного меньше, чем остальные — но этого обычно хватает. Хранение большего количества данных в сессии не рекомендуется, вне зависимости от того, какое хранилище сессии используется в приложении. Особенно следует избегать хранения в сессии сложных объектов (такие как экземпляры модели), так как сервер может не собрать их между запросами, что приведет к ошибке.

Если пользовательские сессии не хранят критически важные данные или нет необходимости в ее сохранности на долгий период (скажем, если она используется только для флеш-сообщений), можно рассмотреть использование ActionDispatch::Session::CacheStore. Он сохранит сессии с использованием реализации кэша, которая была настроена для приложения. Преимущество этого заключается в том, что для хранения сессий можно использовать существующую инфраструктуру кэширования без необходимости дополнительных настроек или администрирования. Недостатком, разумеется, является то, что сессии будут недолговечными и могут исчезнуть в любое время.

Читайте подробнее о хранении сессий в руководстве Безопасность приложений на Rails.

Если вы нуждаетесь в другом механизме хранения сессий, измените его в инициализаторе:

Rails.application.config.session_store :cache_store

Подробности смотрите в config.session_store в руководстве по конфигурации.

Rails устанавливает ключ сессии (имя куки) при подписании данных сессии. Он также может быть изменен в инициализаторе:

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session'

Можете также передать ключ :domain и определить имя домена для куки:

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"

Rails устанавливает (для CookieStore) секретный ключ, используемый для подписания данных сессии, в config/credentials.yml.enc. Он может быть изменен с помощью bin/rails credentials:edit.

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...

Изменение secret_key_base при использовании CookieStore делает все предыдущие сессии невалидными.

В контроллере можно получить доступ к сессии с помощью метода экземпляра session.

Сессии лениво загружаются. Если не получать доступ к сессиям в коде экшна, они не будут загружаться. Следовательно, никогда не придется отключать сессии, просто не обращайтесь к ним, и они будут выполнять свое задание.

Значения сессии хранятся, используя пары ключ/значение, подобно хэшу:

class ApplicationController < ActionController::Base

  private

  # Находим пользователя с ID, хранящимся в сессии с ключом
  # :current_user_id Это обычный способ обрабатывать вход пользователя
  # в приложении на Rails; вход устанавливает значение сессии, а
  # выход убирает его.
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

Чтобы что-то хранить в сессии, просто присвойте это ключу, как в хэше:

class LoginsController < ApplicationController
  # "Создаем" логин (при входе пользователя)
  def create
    if user = User.authenticate(params[:username], params[:password])
      # Сохраняем ID пользователя в сессии, так что он может быть использован
      # в последующих запросах
      session[:current_user_id] = user.id
      redirect_to root_url, status: :see_other root_url
    end
  end
end

Чтобы убрать что-то из сессии, удалите пару ключ/значение:

class LoginsController < ApplicationController
  # "Удаляем" логин (при выходе пользователя)
  def destroy
    # Убираем id пользователя из сессии
    session.delete(:current_user_id)
    # Очистить мемоизированного текущего пользователя
    @_current_user = nil
    redirect_to root_url, status: :see_other
  end
end

Для сброса текущей сессии, используйте reset_session.

Flash — это специальная часть сессии, которая очищается с каждым запросом. Это означает, что сохраненные там значения будут доступны только в следующем запросе, что полезно для передачи сообщений об ошибках и т.п.

К флэшу можно получить доступ с помощью метода flash. Подобно сессии, флэш представлен хэшем.

Давайте рассмотрим случай логаута в качестве примера. Контроллер может послать сообщение, которое будет отображено пользователю при следующем запросе:

class LoginsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url
  end
end

Отметьте, что также возможно назначить сообщение флэш как часть перенаправления. Можно назначить :notice, :alert или общего назначения :flash:

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }

Экшн destroy перенаправляет на root_url приложения, где будет отображено сообщение. Отметьте, что от следующего экшна полностью зависит решение, будет ли он или не будет что-то делать с тем, что предыдущий экшн вложил во flash. Принято отображать любые сообщения об ошибке или уведомления из flash в макете приложения:

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>

    <!-- дальнейшее содержимое -->
  </body>
</html>

В этом случае, если экшн установил сообщения уведомления или предупреждения, макет отобразит их автоматически.

Можно передать все, что только сессия может хранить; вы не ограничены уведомлениями или предупреждениями:

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

Если хотите, чтобы значение flash было перенесено в другой запрос, используйте flash.keep:

class MainController < ApplicationController
  # Давайте скажем этому экшну, соответствующему root_url, что хотим
  # все запросы сюда перенаправить на UsersController#index. Если
  # экшн установил flash и направил сюда, значения в нормальной ситуации
  # будут потеряны, когда произойдет другой редирект, но Вы можете
  # использовать 'keep', чтобы сделать его персистентным для другого запроса.
  def index
    # Все значения flash будут персистентными.
    flash.keep

    # Можете также использовать ключ для сохранения определенных значений.
    # flash.keep(:notice)
    redirect_to users_url
  end
end

По умолчанию, добавление значений во flash делает их доступными для следующего запроса, но иногда хочется иметь доступ к этим значениям в том же запросе. Например, если экшн create проваливается при сохранении ресурса, и будет отрендерен непосредственно макет new, что не приведет к новому запросу, но все равно можно отобразить сообщение, используя flash. Чтобы это сделать, используйте flash.now так же, как используете обычный flash:

class ClientsController < ApplicationController
  def create
    @client = Client.new(client_params)
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

Приложение может хранить небольшое количество данных у клиента — в так называемых куки — которые будут персистентными между запросами и даже сессиями. Rails обеспечивает простой доступ к куки посредством метода cookies, который — очень похож на session — и работает как хэш:

class CommentsController < ApplicationController
  def new
    # Автозаполнение имени комментатора, если оно хранится в куки.
    @comment = Comment.new(author: cookies[:commenter_name])
  end

  def create
    @comment = Comment.new(comment_params)
    if @comment.save
      flash[:notice] = "Thanks for your comment!"
      if params[:remember_name]
        # Запоминаем имя комментатора.
        cookies[:commenter_name] = @comment.author
      else
        # Удаляем из куки имя комментатора, если оно есть.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

Отметьте, что если для удаления значений сессии можно установить ключ в nil, то для удаления значений куки следует использовать cookies.delete(:key).

Rails также предоставляет подписанные куки и зашифрованные куки для хранения чувствительных данных. В подписанные куки добавляется криптографическая сигнатура значений куки для защиты их целостности. Зашифрованные куки шифруют значения в дополнение к их подписи, поэтому они не могут быть прочитаны пользователем. За подробностями обратитесь к документации API.

Эти специальные куки используют сериализатор для сериализации назначенных значений в строки и десериализации их в объекты Ruby при чтении.

Можно определить, какой сериализатор использовать:

Rails.application.config.action_dispatch.cookies_serializer = :json

Для новых приложений сериализатором по умолчанию является :json. Для совместимости со старыми приложениями с существующими куки, используется :marshal, когда не определена опция serializer.

Также можно установить этой опции :hybrid, в этом случае Rails десериализует существующие (сериализованные Marshal) куки при чтении и перезапишет их в формате JSON. Это полезно при миграции существующих приложений на сериализатор :json.

Также возможно передать произвольный сериализатор, откликающийся на load и dump:

Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer

При использовании сериализатора :json или :hybrid, следует знать, что не все объекты Ruby могут быть сериализованы как JSON. Например, объекты Date и Time будут сериализованы как строки, и у хэшей ключи будут преобразованы в строки.

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end

  def read_cookie
    cookies.encrypted[:expiration_date] # => "2014-03-20"
  end
end

Желательно, чтобы в куки хранились только простые данные (строки и числа). Если храните сложные объекты, вам необходимо преобразовывать вручную при чтении значений в последующих запросах.

Если вы храните сессию в куки, все перечисленное также применяется к хэшам session и flash.

ActionController позволяет очень просто рендерить данные XML или JSON. Если сгенерируете контроллер с помощью скаффолда, то он будет выглядеть следующим образом.

class UsersController < ApplicationController
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render xml: @users }
      format.json { render json: @users }
    end
  end
end

Отметьте, что в вышеописанном коде использован render xml: @users, а не render xml: @users.to_xml. Если объект не String, то Rails автоматически вызовет to_xml.

Фильтры — это методы, которые запускаются «до», «после» или «до и после» экшна контроллера.

Фильтры наследуются, поэтому, если вы установите фильтр в ApplicationController, он будет запущен в каждом контроллере вашего приложения.

Фильтры «before» регистрируются с помощью before_action. Они могут прерывать цикл запроса. Обычный фильтр «before» — это, например, тот, который требует, чтобы пользователь был авторизован для запуска экшна. Метод фильтра можно определить следующим образом:

class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def require_login
    unless logged_in?
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url # прерывает цикл запроса
    end
  end
end

Метод просто записывает сообщение об ошибке во flash и перенаправляет на форму авторизации, если пользователь не авторизовался. Если фильтр «before» рендерит или перенаправляет, экшн не запустится. Если есть дополнительные фильтры в очереди, они также будут отменены.

В этом примере фильтр добавлен в ApplicationController, и поэтому все контроллеры в приложении наследуют его. Это приводит к тому, что всё в приложении требует, чтобы пользователь был авторизован, чтобы пользоваться им. По понятным причинам (пользователь не сможет зарегистрироваться в первую очередь!), не все контроллеры или экшны должны требовать его. Вы можете не допустить запуск этого фильтра перед определенными экшнами с помощью skip_before_action:

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

Теперь, экшны LoginsController new и create будут работать как раньше, без требования к пользователю быть зарегистрированным. Опция :only используется для пропуска фильтра только для этих экшнов, а также есть опция :except, которая работает наоборот. Эти опции можно использовать и при добавлении фильтров, поэтому необходимо добавить фильтр, который запускается только для выбранных экшнов в первую очередь.

Вызов одного и того же фильтра несколько раз с разными опциями не будет работать, поскольку последнее определение фильтра перезапишет предыдущие.

В дополнение к фильтрам «before», можно запустить фильтры после того, как экшн был выполнен, или «и до, и после».

Фильтры «after» регистрируются с помощью after_action. Они похожи на «before», но поскольку экшн уже был запущен, у них есть доступ к данным отклика, которые будут отосланы клиенту. Очевидно, фильтры «after» не могут остановить экшн от запуска. Обратите внимание, что фильтры «after» выполняются только после успешного выполнения экшна, но не при возникновении исключения в цикле запроса.

Фильтры «around» регистрируются с помощью around_action. Они ответственны за запуск связанных с ними экшнов с помощью yield, подобно тому, как работают промежуточные программы Rack.

Например, на веб-сайте, где для изменений есть процедура утверждения информации, администратор может легко их просмотреть, применив их внутри транзакции:

class ChangesController < ApplicationController
  around_action :wrap_in_transaction, only: :show

  private

  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      begin
        yield
      ensure
        raise ActiveRecord::Rollback
      end
    end
  end
end

Отметьте, что фильтры «around» также оборачивают рендеринг. В частности, в вышеуказанном примере, если вью сама начнет считывать из базы данных (например через скоуп), она это осуществит внутри транзакции, предоставив, таким образом, данные для предварительного просмотра.

Можно не вызывать yield и создать отклик самостоятельно, в этом случае экшн не будет запущен.

Хотя наиболее распространенный способ использование фильтров — это создание private методов и использование before_action, after_action или around_action для их добавления, есть два других способа делать то же самое.

Первый — это использовать блок прямо в методах *_action. Блок получает контроллер как аргумент. Фильтр require_login может быть переписан с использованием блока:

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url
    end
  end
end

Отметьте, что фильтр в этом случае использует метод send, так как logged_in? является private, и фильтр не запустится в области видимости контроллера. Это не рекомендуемый способ применения такого особого фильтра, но в простых задачах он может быть полезен.

В частности, для around_action в блок также вкладывается action:

around_action { |_controller, action| time(&action) }

Второй способ — это использовать класс (фактически, подойдет любой объект, реагирующий правильными методами) для управления фильтрацией. Это полезно для более сложных задач, которые не могут быть осуществлены предыдущими двумя способами по причине трудности читаемости и повторного использования. Как пример, можете переписать фильтр авторизации снова, использовав класс:

class ApplicationController < ActionController::Base
  before_action LoginFilter
end

class LoginFilter
  def self.before(controller)
    unless controller.send(:logged_in?)
      controller.flash[:error] = "You must be logged in to access this section"
      controller.redirect_to controller.new_login_url
    end
  end
end

Опять же, это не идеальный пример для этого фильтра, поскольку он не запускается в области видимости контроллера, а получает контроллер как аргумент. Класс фильтра должен реализовывать метод с тем же именем, что и фильтр, поэтому для фильтра before_action класс должен реализовать метод before, и так далее. Метод around должен иметь yield для выполнения экшна.

Межсайтовая подделка запроса (CSRF, Cross-Site Request Forgery) — это тип атаки, в которой сайт обманом заставляет пользователя сделать запрос на другой сайт, возможно, добавляя, модифицируя или удаляя данные на этом сайте без ведома или прав доступа пользователя.

Первый шаг, чтобы избежать это — убедиться, что все «разрушительные» экшны (создание, обновление и уничтожение) могут быть доступны только не-GET-запросам. Если вы следуете соглашениям RESTful, то уже делаете это. Однако, сайт злоумышленника может также легко послать не-GET-запрос на ваш сайт, поэтому и необходима защита от подделки запроса. Как сказано в названии, он защищает от подделки запроса.

Это можно сделать, добавив неугадываемый токен, известный только вашему серверу, в каждый запрос. При этом способе, если запрос приходит без подходящего токена, ему будет отказано в доступе.

Если вы генерируете подобную форму:

<%= form_with model: @user do |form| %>
  <%= form.text_field :username %>
  <%= form.text_field :password %>
<% end %>

то увидите, как токен будет добавлен в скрытое поле:

<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
       value="67250ab105eb5ad10851c00a5621854a23af5489"
       name="authenticity_token"/>
<!-- fields -->
</form>

Rails добавит этот токен в каждую форму, генерируемую с помощью хелперов форм, таким образом, большую часть времени можете об этом не беспокоиться. Если вы пишете формы вручную или хотите добавить токен по другой причине, это можно сделать, используя метод form_authenticity_token:

form_authenticity_token генерирует валидный аутентификационный токен. Его полезно размещать в тех местах, куда Rails не добавляет его автоматически, например в произвольные вызовы Ajax.

В руководстве Безопасность приложений на Rails имеется более подробная информация об этом, и множество других вопросов, посвященных безопасности, которые вы должны принимать во внимание при разработке веб-приложения.

10. Объекты Request и Response

В каждом контроллере есть два акцессор-метода, указывающих на объекты запроса и отклика, связанные с циклом запроса, находящегося в текущее время на стадии выполнения. Метод request содержит экземпляр ActionDispatch::Request, а метод response возвращает объект отклика, представляющий то, что будет отправлено обратно на клиента.

Объект request содержит множество полезной информации о запросе, полученном с клиента. Чтобы получить полный перечень доступных методов, обратитесь к документации по Rails API и документации по Rack. В числе свойств, доступных для этого объекта, следующие:

Свойство request Назначение
host Имя хоста, используемого для этого запроса.
domain(n=2) Первые n сегментов имени хоста, начиная справа (домен верхнего уровня).
format Тип содержимого, запрошенного с клиента.
method Метод HTTP, использованного для запроса.
get?, post?, patch?, put?, delete?, head? Возвращает true, если метод HTTP — это GET/POST/PATCH/PUT/DELETE/HEAD.
headers Возвращает хэш, содержащий заголовки, связанные с запросом.
port Номер порта (целое число), использованного для запроса.
protocol Возвращает строку, содержащую использованный протокол плюс «://», например «http://».
query_string Часть URL со строкой запроса, т.е. все после «?».
remote_ip Адрес IP клиента.
url Полный URL, использованный для запроса.

Rails собирает все параметры, посланные вместе с запросом, в хэше params, были ли они посланы как часть строки запроса, либо в теле запроса post. У объекта request имеется три акцессора, которые предоставляют доступ к этим параметрам в зависимости от того, откуда они пришли. Хэш query_parameters содержит параметры, посланные как часть строки запроса, в то время как хэш request_parameters содержит параметры, посланные как часть тела post. Хэш path_parameters содержит параметры, распознанные роутингом как часть пути, ведущего к определенному контроллеру и экшну.

Объект response (отклик) обычно не используется напрямую, а создается во время выполнения экшна и рендеринга данных, которые посылаются обратно пользователю, но иногда — например, в последующем фильтре — бывает полезно иметь доступ к отклику напрямую. Некоторые из этих акцессор-методов имеют сеттеры, позволяющие изменять их значения. Чтобы получить полный перечень доступных методов, обратитесь к документации по Rails API и документации по Rack.

Свойство response Назначение
body Это строка данных, которая будет возвращена клиенту. Чаще всего это HTML.
status Код статуса HTTP для отклика, например 200 для успешного запроса или 404 для ненайденного файла.
location URL, по которому клиент будет перенаправлен, если указан.
content_type Тип содержимого отклика.
charset Кодировка, используемая для отклика. По умолчанию это «utf-8».
headers Заголовки, используемые для отклика.

Если хотите установить произвольные заголовки для отклика, то response.headers — как раз то место, что нужно. Атрибут headers — это хэш, который связывает имена заголовков с их значениями, а Rails устанавливает некоторые из них автоматически. Если нужно добавить или изменить заголовок, просто назначьте его response.headers следующим образом:

response.headers["Content-Type"] = "application/pdf"

В вышеприведенном случае более очевидным было бы использование сеттера content_type.

Rails поставляется с тремя встроенными механизмами аутентификации HTTP:

  • Базовая аутентификация
  • Дайджест-аутентификация
  • Аутентификация по токену

Базовая аутентификация HTTP — это аутентификационная схема, поддерживаемая большинством браузеров и других клиентов HTTP. Как пример, рассмотрим раздел администрирования, который доступен только при вводе имени пользователя и пароля в основном диалоговом окне браузера. Использование встроенной аутентификации достаточно простое и требует использования одного метода http_basic_authenticate_with.

class AdminsController < ApplicationController
  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end

Благодаря этому можно создавать именованные контроллеры, наследуемые от AdminsController. Таким образом, предварительный фильтр будет запущен для всех экшнов в этих контроллерах, защищая их с помощью базовой аутентификации HTTP.

Дайджест-аутентификация HTTP превосходит базовую аутентификацию, так как она не требует от клиента посылать незашифрованный пароль по сети (хотя базовая аутентификация HTTP безопасна через HTTPS). Использовать дайджест-аутентификацию с Rails просто, и это потребует только один метод authenticate_or_request_with_http_digest.

class AdminsController < ApplicationController
  USERS = { "lifo" => "world" }

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_digest do |username|
        USERS[username]
      end
    end
end

Как мы видим из примера, блок authenticate_or_request_with_http_digest принимает только один аргумент — имя пользователя. И блок возвращает пароль. Возврат false или nil из authenticate_or_request_with_http_digest вызовет провал аутентификации.

Аутентификация HTTP по токену — это схема для использования токенов Bearer в заголовке HTTP Authorization. Имеется множество доступных форматов токенов, но их описание выходит за рамки этой документации.

В качестве примера, предположим вы хотите использовать аутентификационный токен, выпущенный заранее, для аутентификации и доступа. Реализация аутентификации по токену с помощью Rails довольно простая и требует использования лишь одного метода, authenticate_or_request_with_http_token.

class PostsController < ApplicationController
  TOKEN = "secret"

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
      end
    end
end

Как видно из примера, блок authenticate_or_request_with_http_token принимает два аргумента — токен и Hash, содержащий опции, которые были взяты из заголовка HTTP Authorization. Блок должен вернуть true, если аутентификация успешная. Возврат false или nil в нем вызовет неудачу в аутентификации.

Иногда хочется послать пользователю файл вместо рендеринга страницы HTML. Все контроллеры в Rails имеют методы send_data и send_file, которые направляют данные на клиента. send_file — это удобный метод, который позволяет указать имя файла на диске, а он направит содержимое этого файла вам.

Чтобы направить данные на клиента, используйте send_data:

require "prawn"
class ClientsController < ApplicationController
  # Генерирует документ PDF с информацией на клиента и возвращает
  # его. Пользователь получает PDF как загрузку файла.
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              filename: "#{client.name}.pdf",
              type: "application/pdf"
  end

  private
    def generate_pdf(client)
      Prawn::Document.new do
        text client.name, align: :center
        text "Address: #{client.address}"
        text "Email: #{client.email}"
      end.render
    end
end

Экшн download_pdf в примере вызовет private метод, который фактически сгенерирует документ PDF и возвратит его как строку. Эта строка будет направлена клиенту как загрузка файла, и пользователю будет предложено имя файла. Иногда при потоковой передаче файлов пользователю может не потребоваться загрузка файла. Возьмите, например, изображения, которые могут быть встроены в страницы HTML. Чтобы сказать браузеру, что файл не предназначен для скачивания, нужно установить опцию :disposition как «inline». Противоположное дефолтное значение этой опции — «attachment».

Если хотите отправить файл, уже существующий на диске, используйте метод send_file.

class ClientsController < ApplicationController
  # Потоковая передача файла, который уже был сгенерирован и сохранен на диск.
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              filename: "#{client.name}.pdf",
              type: "application/pdf")
  end
end

Это прочтет и передаст файл блоками в 4 Кбайт за раз, избегая загрузки в память сразу целого файла. Можно отключить потоковую передачу с помощью опции :stream или отрегулировать размер блока с помощью опции :buffer_size.

Если не указан :type, он будет определяться по расширению файла, указанного в :filename. Если для расширения не зарегистрирован тип содержимого, будет использован application/octet-stream.

Будьте осторожны, когда используете данные, пришедшие с клиента (params, куки и т.д.), для обнаружения файла на диске, так как есть риск безопасности в том, что кто-то может получить доступ к файлам, к которым иметь он не должен.

Не рекомендуется передавать в потоке статичные файлы через Rails, если можно вместо этого разместить их в папке public на веб-сервере. Более эффективно разрешить пользователям скачивать файлы напрямую, используя Apache или другой веб-сервер, сохраняя запрос от ненужного прогона через весь стек Rails.

Хотя send_data работает прекрасно, если вы создаете приложение на принципах RESTful, наличие отдельных экшнов для загрузок файла обычно не требуется. В терминологии REST файл PDF из вышеприведенного примера можно рассматривать еще одним представлением ресурса client. Rails предоставляет простой и наглядный способ осуществления загрузок в стиле RESTful. Вот как можно переписать пример, чтобы загрузка PDF была частью экшна show без какой-либо потоковой передачи:

class ClientsController < ApplicationController
  # Пользователь может запросить получение этого ресурса как HTML или PDF.
  def show
    @client = Client.find(params[:id])

    respond_to do |format|
      format.html
      format.pdf { render pdf: generate_pdf(@client) }
    end
  end
end

Чтобы этот пример заработал, нужно добавить PDF тип MIME в Rails. Это можно сделать, добавив следующую строчку в файл config/initializers/mime_types.rb:

Mime::Type.register "application/pdf", :pdf

Конфигурационные файлы не перезагружаются с каждым запросом, поэтому необходимо перезапустить сервер для того, чтобы их изменения вступили в силу.

Теперь пользователь может запрашивать получение версии в PDF, просто добавив «.pdf» в URL:

Rails позволяет отдавать в потоке не только файлы. Фактически, в объекте отклика можно отдать все, что хотите. Модуль ActionController::Live позволяет создать персистентное соединение с браузером. Используя этот модуль, можно послать в браузер произвольные данные в определенные моменты времени.

Включение ActionController::Live в класс вашего контроллера предоставит всем экшнам контроллера возможность отдавать данные в потоке. Этот модуль можно включить следующим образом:

class MyController < ActionController::Base
  include ActionController::Live

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello worldn"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

Вышеприведенный код будет поддерживать персистентное соединение с браузером и пошлет 100 сообщений "hello worldn", раз в секунду каждое.

В вышеприведенном примере нужно обратить внимание на ряд вещей. Необходимо убедиться, что потоковый отклик будет закрыт. Если забыть закрыть, поток оставит навсегда открытым сокет. Также необходимо установить тип содержимого text/event-stream до записи в поток отклика. Это так, потому что заголовки не могут быть записаны после того, как отклик был совершен (когда response.committed? возвращает истинное значение), которое возникает, когда вызывается write или commit для потокового отклика.

Предположим, мы создаем машину караоке, и пользователь хочет получить слова для определенной песни. В каждом Song имеется определенное количество строчек, и у каждой строчки есть время num_beats для завершения пения.

Если мы хотим возвращать слова по принципу караоке (посылая строчку, только когда певец закончил предыдущую), можно использовать ActionController::Live следующим образом:

class LyricsController < ActionController::Base
  include ActionController::Live

  def show
    response.headers['Content-Type'] = 'text/event-stream'
    song = Song.find(params[:id])

    song.each do |line|
      response.stream.write line.lyrics
      sleep line.num_beats
    end
  ensure
    response.stream.close
  end
end

Вышеприведенный код посылает следующую строчку только после того, как певец завершил предыдущую строчку.

Потоковая передача произвольных данных – чрезвычайно мощный инструмент. Как показано в предыдущих примерах, можно выбирать, когда и что посылать в потоковом отклике. Однако, также необходимо отметить следующие вещи:

  • Каждый потоковый отклик создает новый тред и копирует тредовые локальные переменные из текущего треда. Наличие большого количество тредовых локальных переменных может отрицательно сказаться на производительности. Большое количество тредов также препятствует производительности.
  • Незакрытие потокового отклика оставит соответствующий сокет открытым навсегда. Убедитесь, что вызываете close при использовании потокового отклика.
  • Серверы WEBrick буферизируют все отклики, поэтому включение ActionController::Live не будет работать. Необходимо использовать веб-сервер, не буферизирующий отклики автоматически.

Rails ведет лог-файл для каждой среды в папке log. Это чрезвычайно полезно при отладке того, что происходит в приложении, но в реальной жизни может быть не нужно хранение каждого бита информации в лог-файле.

Можно фильтровать чувствительные параметры запроса в файлах лога, присоединив их к config.filter_parameters в настройках приложения. Эти параметры будут помечены в логе как [FILTERED].

config.filter_parameters << :password

Предоставленные параметры будут отфильтрованы с помощью частично соответствующего регулярного выражения. Rails добавляет список фильтров по умолчанию, включающий :passw, :secret и :token, в соответствующем инициализаторе (initializers/filter_parameter_logging.rb), чтобы обрабатывать типичные параметры приложения, такие как password, password_confirmation и my_token.

Иногда нужно фильтровать из файлов лога некоторые чувствительные места расположения, на которые перенаправляет приложение. Это можно осуществить с использованием конфигурационной опции config.filter_redirect:

config.filter_redirect << 's3.amazonaws.com'

Ей можно передать строку, регулярное выражение или массив из них.

config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

Соответствующие URL будут помечены как ‘[FILTERED]’.

Скорее всего, ваше приложение будет содержать программные ошибки или, другими словами, вызывать исключения, которые нужно обработать. Например, если пользователь переходит по ссылке на ресурс, который больше не существует в базе данных, Active Record вызовет исключение ActiveRecord::RecordNotFound.

Дефолтный обработчик исключений Rails отображает сообщение «500 Server Error» для всех исключений. Если запрос сделан локально, отображается прекрасная трассировка и добавляется дополнительная информация, чтобы можно было выяснить, что пошло не так, и разобраться с этим. Если запрос был удаленным, Rails отобразит пользователю лишь простое сообщение «500 Server Error», или «404 Not Found», если была проблема с роутингом или запись не была найдена. Иногда может понадобиться настроить, как эти ошибки будут перехвачены и как они будут отображены пользователю. В приложении на Rails доступны несколько уровней обработки исключений:

По умолчанию в среде production приложение будет рендерить или 404, или 500 сообщение об ошибке. В среде development будут просто вызываться все необрабатываемые исключения. Эти сообщения содержатся в статичных файлах HTML в папке public, в 404.html и 500.html соответственно. Можно настроить эти файлы, добавив дополнительную информацию и стили, но помните, что они статичные; т.е. нельзя использовать ERB, SCSS, CoffeeScript или макеты для них.

Если хотите сделать нечто более сложное при перехвате ошибок, можете использовать rescue_from, который обрабатывает исключения определенного типа (или нескольких типов) во всем контроллере и его подклассах.

Когда возникает исключение, которое перехватывается директивой rescue_from, объект исключения передается в обработчик. Обработчик может быть методом или объектом Proc, переданным опции :with. Также можно использовать блок вместо объекта Proc.

Вот как можно использовать rescue_from для перехвата всех ошибок ActiveRecord::RecordNotFound и что-то с ними делать.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  private
    def record_not_found
      render plain: "404 Not Found", status: 404
    end
end

Конечно, этот пример далеко недоработан, и ничуть не улучшает обработку исключений по умолчанию, но раз вы уже перехватили все эти исключения, то вольны делать с ними все, что хотите. Например, можете создать свои классы исключений, которые будут вызваны, когда у пользователя нет доступа в определенные разделы вашего приложения:

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized

  private
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      redirect_back(fallback_location: root_path)
    end
end

class ClientsController < ApplicationController
  # Проверим, что пользователь имеет права авторизации для доступа к клиентам.
  before_action :check_authorization

  # Отметьте, как экшны не беспокоятся об авторизационных делах.
  def edit
    @client = Client.find(params[:id])
  end

  private
    # Если пользователь не авторизован, просто вызываем исключение.
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

Использование rescue_from c Exception или StandardError вызовет серьезные побочные эффекты, поскольку это препятствует Rails правильно обрабатывать исключения. Таким образом, это не рекомендуется делать, если нет для того веской причины.

При запуске в среде running production все ошибки ActiveRecord::RecordNotFound рендерят страницу ошибки 404. Если вам не нужно другое поведение, их не нужно обрабатывать.

Некоторые исключения перехватываемы только из класса ApplicationController, так как они вызываются до того, как контроллер будет инициализирован и экшны будут выполнены.

Если необходимо обеспечить доступ к определенному контроллеру только через HTTPS, нужно сделать это, включив промежуточную программу ActionDispatch::SSL через config.force_ssl в конфигурациях среды.

Exception Handling in Ruby

In Ruby, error handling works like this; all exceptions and errors are extensions of the Exception class. While this may seem intuitive, exception handling in Ruby is a touch more nuanced than you might expect thanks to the designed hierarchy of Ruby exceptions.

The begin-rescue

Similar to PHP’s try-catch handler, Ruby’s exception handling begins with the begin-rescue block. In a nutshell, the begin-rescue is a code block in Ruby that can be used to deal with raised exceptions without interrupting the Ruby program execution. In other words, you can begin to execute a block of code, and rescue any exceptions that are raised.

Rescuing Exceptions

In Ruby by default, begin-rescue rescues every instance of the StandardError class. This includes no method errors, type errors, runtime errors, and every custom error that is intended to be rescued within a Ruby application (see Raising Exceptions in Ruby for more information). To rescue every StandardError, simply wrap the designated section of code in a begin-rescue block:

begin
  # ...
rescue => e
  # ...
end

In Ruby when a StandardError exception is raised within the begin block, an instance of it will be passed to the rescue block as the variable e (for more information about the structure of Ruby’s Exception class, see Raising Exceptions in Ruby).

Rescuing Specific Exceptions

While rescuing every exception raised in your Ruby app is great for simplistic implementations—such as generalizing API error responses—best practice is to rescue for specific exceptions. To do this, let’s rewrite the generic Ruby begin-rescue block above to specifically rescue StandardError exceptions:

begin
  # ...
rescue StandardError => e
  # ...
end

Although the difference may be subtle, by following the rescue command with a class name, then only exceptions of the defined type will be rescued. For example, if we wanted to rescue all argument errors, we could structure our begin-rescue block like this:

begin
  # ...
rescue ArgumentError => e
  # ...
end

But, what if we want to rescue more than one exception type? Much like an if-elsif-else chain, a begin-rescue block can have multiple rescues, which when combined with a check for the StandardError class, allows you to logically adapt to any and all issues that may arise:

begin
  # ...
rescue ArgumentError => e
  # ...
rescue TypeError => e
  # ...
rescue => e
  # ...
end

Rescuing All Exceptions

While it may be tempting to rescue every child of the Exception class, it is generally considered bad practice due to the way the Ruby exception hierarchy is structured. The reason for this is that, while all Ruby exceptions and errors are an extension of the Exception class, many of them are reserved for use internally by Ruby. For example, SignalException::Interrupt is used to signal that Ctrl-C has been detected during the execution of a script.

If you were to rescue every child of the Exception class, then the Interrupt exception wouldn’t work as expected. That said, if you do want to rescue every exception that is raised in Ruby, the same begin-rescue block can be used to specifically rescue all Exception exceptions:

begin
  # ...
rescue Exception => e
  # ...
end

Using the above begin-rescue block in Ruby will rescue every exception and error that is raised, from interrupts to syntax errors, and even memory errors, so use it sparingly and with caution.

How to check Ruby syntax to identify exceptions

rescueclauses are used to tell Ruby which exception or types of exceptions we want to handle. The syntax for therescuestatement is:

begin
    # may raise an exception
rescue AnException
    # exception handler
rescue AnotherException
    # exception handler
else
    # other exceptions
ensure
    # always executed
end

The code betweenbeginandrescueis where a Ruby exception can occur. If an exception is encountered, the code inside therescueclause gets executed. For eachrescueclause, the raised Ruby exception is compared against each parameter and the match succeeds if the exception in the clause is the same as or a superclass of the thrown exception.

If the thrown Ruby exception does not match any of the specified exception types, theelse block gets executed. Theensureblock is always executed whether a Ruby exception occurs or not.

As an example:

#!/usr/bin/ruby
begin
   file = open("/tmp/myfile")
rescue Errno::ENOENT
   p "File not found"
else
   p "File opened"
end

In the above example, a file is attempted to be opened in thebeginblock. Therescueblock catches a “File not found” Ruby exception in case the file is not found at the location. If the file is found, theelseblock gets executed.

Running the above Ruby code produces the following result if the file is not found:

"File not found"

If the file is found, the following output is produced:

"File not found"

Exception Handling in Ruby on Rails

Generally speaking, the begin-rescue block works as intended in Ruby on Rails. That said, in order to better handle the specific use cases that can come up in the Rails architecture, additional methods have been made available for use within a Ruby on Rails application.

The rescue_from

The rescue_from directive is an exception handler that rescues exceptions raised in controller actions. The rescue_from directive rescues the specified exceptions raised within a controller, and reacts to those exceptions with a defined method. For example, the following controller rescues User::NotAuthorized exceptions and passes them to the deny_access() method:

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :deny_access

  protected
    def deny_access(exception)
    # ...
    end
end

The advantage to rescue_from is that it abstracts the exception handling away from individual controller actions, and instead makes exception handling a requirement of the controller. This not only makes exception handling within controllers more readable, but also more regimented.

Exception Handling in Sinatra

While the additional exception handling within Ruby on Rails is focused on controller exceptions, Sinatra offers a few additional ways to deal with raised exceptions.

Logging Exceptions with dump_errors

Enabled by default, dump_errors is a Sinatra setting that allows exception backtraces to be written directly to STDERR. In the context of a development server, this information can be incredibly valuable, but might be more difficult to act upon in a production environment. When used in conjunction with traditional log aggregation and analysis techniques, however, this is a great way to collect exception data as it happens without needing to reproduce it.

Propagating Exceptions with raise_errors

By default, exceptions raised within Sinatra do not leave the application. What this means is that, when an exception is raised, it is rescued and mapped to internal error handlers. By enabling the raise_errors setting, these exceptions are raised outside of the application, allowing the server handler or Rack middleware to deal with exceptions.

Enable Classy Error Pages with show_exceptions

When working in a development environment, being able to quickly react to exceptions is crucial, which is why exception backtraces and environment information are on-screen by default in these environments. While this setting is turned off in production environments, it can be enabled or disabled by updating the show_exceptions setting.

Custom Error Handling

While Sinatra has built-in support for graceful error handling (error handler), it is sometimes desirable to write custom logic to handle raised errors. To do this, an error block can be used in the same way as a rescue block. For example, to rescue a User::NotAuthorized error, the following directive would work in Sinatra:

error User::NotAuthorized do
  # ...
end

Similarly, if we wanted to rescue all exceptions that are raised in a Sinatra application, we could use the following error handler:

error do
  # ...
end

Check ruby syntax

If you ever need to check the syntax of your Ruby code here is a great way to do so without executing the Ruby code.

This will check the syntax without executing the program:

ruby -c filename.rb

Понравилась статья? Поделить с друзьями:
  • R99 на брелке starline a93 как исправить
  • R6s error at hooking api
  • Rails cap production doctor error
  • R6034 ошибка windows 7
  • Rails aborted standarderror an error has occurred this and all later migrations canceled