New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce Actionable Errors #34788
Introduce Actionable Errors #34788
Conversation
afd77bb
to
726fc8f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome ! 🎉
726fc8f
to
90dc382
Compare
What's empty |
^great holiday pr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great @gsamokovarov! Really excited to see this in Rails 6!! 👏
aea4fa3
to
6dcdd3e
Compare
The `#csrf_meta_tags` and `#token_tag` Action View helper methods are expecting the class in which are included to explicitly define the method `#protect_against_forgery?` or else they will fail with `NoMethodError`. This is a problem if you want to use Action View outside of Rails applications. For example, in rails#34788 I used the `#button_to` helper inside of the error pages templates that have a custom `ActionView::Base` subclass, which did not defined `#protect_against_forgery?` and trying to call the button failed. I had to dig inside of Action View to find-out what's was going on. I think we should either set a default method implementation in the helpers or check for the method definition, but don't explicitly require the presence of `#protect_against_forgery?` in every `ActionViews::Base` subclass as the errors are hard to figure out.
6dcdd3e
to
e4a7885
Compare
I'm disappointed to leave behind both stateful actions and any progress/output channel, but getting a solid foundation in sounds great. |
36eaef8
to
65e47ac
Compare
1d3e589
to
03ba01e
Compare
03ba01e
to
84754f7
Compare
6e28560
to
7b43d22
Compare
7b43d22
to
30e4dd1
Compare
* Introduce `ActionDispatch::ActionableExceptions`. | ||
|
||
The `ActionDispatch::ActionableExceptions` middleware dispatches actions | ||
from `ActiveSupport::ActionableError` descendants. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably expand on what this does though, or what the end goal is that we get out of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we actually expand this below, probably a one line note here would be great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm liking this a lot! Let me know if you need more reviews, I'll help you get this in for Rails 6 ❤️
include ActiveSupport::ActionableError | ||
|
||
action "Run pending migrations" do | ||
ActiveRecord::Tasks::DatabaseTasks.migrate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this location should know about the DatabaseTasks. How about we let action accept a command option instead? E.g.:
action "Run pending migrations", command: "db:migrate"
def action(name, command: nil, &block)
_actions[name] = block || -> { Rails::Command.invoke command }
end
Although then Active Support knows about Rails commands… 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's deal with this part later. I do like the idea that other libraries could extend action
with their own behaviors ala:
# Somewhere in railties
ActiveSupport::ActionableErrors.register_behavior :command do |command|
Rails::Command.invoke command
end
# Back in Active Support:
def action(name, **behaviors, &block)
_actions[name] = block || extract_behavior(behaviors)
end
def extract_behavior(behaviors)
if match = (behaviors.keys & self.class.behaviors.keys).first
callable, value = self.class.behaviors[match], behaviors[match]
-> { callable.call(value) }
end
end
Hm, but then we'll have load order dependencies… 😅 Well, this was fun to think about! I still think we should find a way such that Migration doesn't know about the migrate
task, but that's later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, yeah, I like that we can execute arbitrary code here now and we're not explicitly coupled with commands, but they are kinda easy to invoke if that's your desire.
actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb
Outdated
Show resolved
Hide resolved
module ActionableError | ||
extend Concern | ||
|
||
NonActionable = Class.new(StandardError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we used the class NonActionable < StandardError; end
style?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, will change this.
# module and invoke the +action+ class macro to define the action. An action | ||
# needs a name and a block to execute. | ||
module ActionableError | ||
extend Concern |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we put autoload :ActionableError
beneath Concern instead and skip the require? Because this organization just nullifies the Concern autoload.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems good!
request = ActionDispatch::Request.new(env) | ||
return @app.call(env) unless actionable_request?(request) | ||
|
||
ActiveSupport::ActionableError.dispatch(request.params["error"], request.params["action"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll just reference #35489 in passing, though I'm still not sure if it changes anything for this.
Oh, @kaspth, haven't seen your round of comments. I will take a look at them and respond back in a bit. |
8af39ad
to
82a56df
Compare
Actionable errors let's you dispatch actions from Rails' error pages. This can help you save time if you have a clear action for the resolution of common development errors. The de-facto example are pending migrations. Every time pending migrations are found, a middleware raises an error. With actionable errors, you can run the migrations right from the error page. Other examples include Rails plugins that need to run a rake task to setup themselves. They can now raise actionable errors to run the setup straight from the error pages. Here is how to define an actionable error: ```ruby class PendingMigrationError < MigrationError #:nodoc: include ActiveSupport::ActionableError action "Run pending migrations" do ActiveRecord::Tasks::DatabaseTasks.migrate end end ``` To make an error actionable, include the `ActiveSupport::ActionableError` module and invoke the `action` class macro to define the action. An action needs a name and a procedure to execute. The name is shown as the name of a button on the error pages. Once clicked, it will invoke the given procedure.
82a56df
to
feaaa75
Compare
@kaspth, @vipulnsward Sorry for taking me so long to get back to you. Can you folks take a "final" (final, final 😂) look? |
🎉 🎉 🎉 |
✌️ |
Guys, I just checked my production logs and somebody crafted a request:
They were able to trigger an attempt at running migrations on my production environment. |
What rails version are you running on and do you happen to have |
I am running rails 6.0.2.2. I do not have |
That security hole was fixed in 6.0.3.2. GHSA-c6qr-h5vq-59jc. You should make sure your Rails version is up to date because that is not the only security issue that you have open in your app. |
Ok, thanks for clearing that up. Yes, an update is on the cards
…On Mon, Mar 22, 2021 at 7:54 PM Rafael França ***@***.***> wrote:
That security hole was fixed in 6.0.3.2.
https://github.com/advisories/GHSA-c6qr-h5vq-59jc. You should make sure
your Rails version is up to date because that is not the only security
issue that you have open in your app.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#34788 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAEFWF633NUPAWDYLVT45LTE6N5XANCNFSM4GMEPLZA>
.
|
The idea of actionable errors has been floating around for a few years. First
introduced by @vipulnsward in #26542 (thanks Vipul) and later on expanded
by @causztic and myself in this year's GSoC. The current implementation is the
result of the lessons we learnt from the previous ones and, I hope, provides
the simplest API and implementation.
Actionable errors let's you dispatch actions from Rails' error pages. This
can help you save time if you have a clear action for the resolution of
common development errors.
The de-facto example are pending migrations. Every time pending migrations
are found, a middleware raises an error. With actionable errors, you can
run the migrations right from the error page. Other examples include Rails
plugins that need to run a rake task to setup themselves. They can now
raise actionable errors to run the setup straight from the error pages.
Here is how to define an actionable error:
To make an error actionable, include the
ActiveSupport::ActionableError
module and invoke the
action
class macro to define the action. Actionneeds a name and a procedure to execute. The name is shown as the name of a
button on the error pages. Once clicked, it will invoke the given
procedure. An error can have multiple actions as well.
The actions are dispatched by simple forms. No XHR's or any JS involved in
the process. The current page address is remembered and if the action was
successful, the dispatching middleware will issue a redirect back to it.
This is because to make an "action progress" page we'd need to either:
action
class macro.STD{OUT,ERR}
and display it.In any case, I think we can avoid all of the manutia around progress display and
do a simple post submission for now.
Here are a few screenshots of the error pages of actionable errors:
The builtin pending migrations error is now actionable
A custom actionable error with multiple actions
When an action fails