-
Notifications
You must be signed in to change notification settings - Fork 21.7k
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 explicit way of halting callback chains by throwing :abort. Deprecate current implicit behavior of halting callback chains by returning false
in apps ported to Rails 5.0. Completely remove that behavior in brand new Rails 5.0 apps.
#17227
Introduce explicit way of halting callback chains by throwing :abort. Deprecate current implicit behavior of halting callback chains by returning false
in apps ported to Rails 5.0. Completely remove that behavior in brand new Rails 5.0 apps.
#17227
Conversation
Looking pretty good. I'd actually like to bubble this all the way up to ActiveSupport, though. IMO, it should be a generic feature of the callback code to have "throw :abort" that'll cancel the entire chain. Thanks for working on this! |
e5b3016
to
a443a6c
Compare
@dhh I investigated a little deeper and found that I did not have to add a brand new method to halt the execution of a chain callback: there is already a
So my suggestion is: let's change the default value of The advantage of this method is that it is backward-compatible for the modules that explicitly define a terminator: ->(_,result) { result == false }, meaning that any
As you read the list of files changed by this PR, keep in mind that In this way, I am able Please tell me what you think, and also if you think |
This seems better, but I don't like |
a443a6c
to
2fbd920
Compare
@dhh I'm fine with any name. I have an additional note. With this PR, the behavior does not change. Therefore, if an I think this okay, since it matches what the documentation says and existing code. |
Ultimately, I think you should be able to halt the after_* chain as well. But we don’t need to deal with it in the same PR. On Oct 16, 2014, at 17:02, Claudio B. notifications@github.com wrote:
|
Any reason to use throw / catch vs an exception? We should probably test the performance impact of this change as well. Callbacks are a huge hotspot (since we use them literally everywhere). |
Exceptions should only be when something is going wrong, really. We need a way to control the flow when aborting the callbacks is the expected path.
|
2fbd920
to
2ba0631
Compare
2ba0631
to
5bdd441
Compare
Perhaps it's wrong assumption to make but I'd assume seeing Perhaps |
Halting execution during the before callbacks is the primary use case, and stopping not just the callbacks, but the action itself, from being executed is the primary within that. So given that setup, I still prefer something that doesn't have *_callbacks. "throw halt" sounds a little weird to me. "throw abort" seems nicer. "throw abort" will cancel/reverse the save if its still under the same transaction, so that seems accurate too. |
5bdd441
to
9f07b16
Compare
Hello @dhh @pixeltrix @sferik @wjessop @tomstuart Now that Rails 4.2.rc1 is out, it's probably a good time to continue the discussion on this ticket, which also matches what you guys discussed on Twitter. The recap of this PR is: change the behavior of callbacks so that returning More details in the comments above! 🍭 |
👍 |
9f07b16
to
6f389e0
Compare
@dhh Rebased and conflicts solved! |
Looks good to me, but I'll let @rafaelfranca or someone else on the team have a look as well. |
I've only skimmed, but it sounds like we're missing a deprecation here at the moment? I would expect that a falsey result would emit a deprecation warning, but still halt the chain, for a version. |
Let's get that into 4.2.
|
@dhh the only way to get the deprecation into 4.2 would be to ship this whole feature change in 4.2. And it's way too late for that -- especially considering @tenderlove's very reasonable performance concerns. With a deprecation warning, we can ship this as the right way to do things in 5.0... it's just that the old way will still work (noisily) until 5.1. |
Fine be me too.
|
@matthewd @dhh I'm not 100% sure this PR needs a deprecation policy. Let me explain: the behavior of callback chains that define a terminator does not change after this PR. This PR adds a new behavior for callback chains that do not define a terminator. Before this PR, if your callback chain did not define a terminator, then the callback chain could never halted. After this PR, if your callback chain does not define a terminator, then you have the possibility to halt the callback chain by throwing I think a deprecation policy will only be needed once we change existing behaviors: for instance, if we decide that returning Let me know what you think! |
Let's expand this PR to include the deprecation. The main benefit of this switch is not using throw :abort when you know you want to halt, but to avoid halting when the callback accidentally returns false.
|
6f389e0
to
4318bc9
Compare
@dhh I have followed your suggestion and included the deprecation in the three places where callback chains can be halted by returning false:
As a result of this PR, returning
Please tell me if you'd like to rephrase the message. The message should make clear that returning |
Looks good to me 👍
|
==> #20612 |
Fixes rails#21122 - does not change any current behavior; simply reflects the fact that two conditions of the if/else statement are never reached. The reason is rails#17227 which adds a default terminator to AS::Callbacks. Therefore, even callback chains that do not define a terminator now have a terminator, and `chain_config.key?(:terminator)` is always true. Of course, if no terminator was defined, then we want this new default terminator not to do anything special. What the terminator actually does (or should do) is discussed in rails#21218 but the simple fact that a default terminator exists makes this current PR valid.
Fixes rails#21122 - does not change any current behavior; simply reflects the fact that two conditions of the if/else statement are never reached. The reason is rails#17227 which adds a default terminator to AS::Callbacks. Therefore, even callback chains that do not define a terminator now have a terminator, and `chain_config.key?(:terminator)` is always true. Of course, if no terminator was defined, then we want this new default terminator not to do anything special. What the terminator actually does (or should do) is discussed in rails#21218 but the simple fact that a default terminator exists makes this current PR valid.
Fixes rails#21122 - does not change any current behavior; simply reflects the fact that two conditions of the if/else statement are never reached. The reason is rails#17227 which adds a default terminator to AS::Callbacks. Therefore, even callback chains that do not define a terminator now have a terminator, and `chain_config.key?(:terminator)` is always true. Of course, if no terminator was defined, then we want this new default terminator not to do anything special. What the terminator actually does (or should do) is discussed in rails#21218 but the simple fact that a default terminator exists makes this current PR valid.
Fixes rails#21122 - does not change any current behavior; simply reflects the fact that two conditions of the if/else statement are never reached. The reason is rails#17227 which adds a default terminator to AS::Callbacks. Therefore, even callback chains that do not define a terminator now have a terminator, and `chain_config.key?(:terminator)` is always true. Of course, if no terminator was defined, then we want this new default terminator not to do anything special. What the terminator actually does (or should do) is discussed in rails#21218 but the simple fact that a default terminator exists makes this current PR valid. *Note* that the conditional/simple methods have not been removed in AS::Conditionals::Filter::After because of `:skip_after_callbacks_if_terminated` which lets a user decide **not** to skip after callbacks even if the chain was terminated.
rails/rails#17227 introduces a change to the way execution is halted by a before callback. Instead of the before-hook returning false, it must `throw(:abort)`. This feature is opt-in for projects upgrading from older versions of Rails, so we need to handle both new and upgrading apps by checking whether they are opted into this new behavior yet.
Is there anyway the scope of this could be increased to allow you to throw in controller methods that are run as guard clauses rather than before_filters to abort the rest of the action. It would be really nice for avoiding DoubleRenderErrors. Trivial Case: class ThingsController < ApplicationController
def index
ensure_authorized
end
protected
def ensure_authorized
redirect_to root_url
throw(:abort)
end
end I will gladly submit a pull request if it would be accepted, but don't want to waste time. |
@denniscollective I think what you suggest would be overwhelming, meaning that we would have to wrap every action inside a In the example above, what would you like to happen if the user is not authorized? def index
if authorized
# .. render view or any behavior that you desire
else
# .. redirect_to with a flash or any behavior that you desire
end
end If you really want the behavior provided by |
@claudiob I used |
@yamanaltereh Hello! Can you provide a little more context or a snippet of code? If you found a bug, a bug report would also be very useful. Thanks! |
To integrate with Active Record you just need to `include MailForm::Delivery` and implement the `headers` method. An email should be sent whenever the model is created successfully. That was broken since we introduced these extra conditionals to fix the delivery of MailForm, due to the change in how Rails handles callbacks abortion to not rely on the `false` value returning from `before_*` callbacks, to explicitly require the code to `throw(:abort)` instead. This was happening for objects inheriting from `MailForm::Base` so it was working fine there, but it broke Active Record integrations since it was just doing that for the `*_deliver` callbacks, and because of the way the check was introduced, it was trying to use those same callbacks inside AR, and they don't exist there, resulting in errors: NoMethodError (undefined method `before_deliver'... This change to callbacks was introduced by Rails 5 (rails/rails#17227), which means we can safely change the logic to `throw(:abort)` for both `deliver` and `create` callbacks going forward. Include some tests that simulate Active Record behavior through Active Model and `create` callbacks, since we didn't have any specific test coverage there. Closes #56.
…save from happening (new in rails 5 - rails/rails#17227)
Update (2015-01-02): a gist with the suggested release notes to add to Rails 5.0 after this commit is available at https://gist.github.com/claudiob/614c59409fb7d11f2931
Stems from discussion with @dhh at https://groups.google.com/forum/#!topic/rubyonrails-core/mhD4T90g0G4
@dhh – I created this work-in-progress PR to continue the conversation with some code.
Could you tell me if this is what you intended by using
:throw
with a symbol?I have quite clear how to add the
throw
to the code in ActiveJob, but not asmuch where to
catch
it in ActiveSupport. For now I have this code:but maybe the
catch
is better located somewhere at a deeper level of the callback stack.@dhh Thoughts? Once I have a clearer idea on how to proceed, I can complete
this PR with tests, documentations, etc. Thanks!