Skip to content
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

Inheritance #1

Merged
merged 10 commits into from
Sep 19, 2016
Merged

Inheritance #1

merged 10 commits into from
Sep 19, 2016

Conversation

vlymar
Copy link

@vlymar vlymar commented Sep 15, 2016

After upgrading to rails 5 we found that the way wicked_pdf prepends ActionController::Base#render causes an infinite loop when combined with another gem prepending render.

I believe our issue is generally related to mileszs#111, though this PR focuses on prepend behavior.

The loop happens because wicked_pdf is not using the traditional prepend inheritance pattern, which looks like this

class Base
  def render
  end
end

class Patch
  def render
    patched_behavior
    super
  end
end

Base.prepend(Patch)

Instead, PdfHelper aliases the current render method away, then defines its own render method on the base class, which then calls the new render_with_wicked_pdf method.

If any other gem prepends its own render method using the inheritance pattern, before wicked_pdf prepends PdfHelper, this happens:

  1. ActionController::Base is loaded, and its render method is defined.
  2. OtherGem has its patch prepended, putting its render method in front of ActionController::Bases render method in the lookup chain. (OtherGem calls super to get to the original render method)
  3. wicked_pdf has PdfHelper prepended:
  4. PdfHelper.prepended aliases OtherGem#render to render_without_wicked.
  5. PdfHelper.prepended re-defines render on ActionController::Base using base_eval.
  6. render is called from a controller action:
  7. OtherGem#render is called, and it calls super
  8. PdfHelper's version of #render is called, and it calls render_with_wicked
  9. render_with_wicked calls render_without_wicked
  10. render_without_wicked is an alias for OtherGem#render
  11. infinite loop.

I've included a test that demonstrates this behavior. Its quite ugly as it has to replicate what railtie.rb does after another gem is prepended, which i couldn't figure out how to do since all of wicked is loaded before the tests run, so my solution was to just demonstrate it with a mock ActionController::Base class.

I'm new to test-unit so please let me know if these tests can be cleaned up.

vlymar added 2 commits September 14, 2016 15:05
The PdfHelper module is prepended to ActionController::Base, but the
proper ruby prepend patter is not used. Instead of defining the method
to be overridden in the module and prepending that module in the target
class, the module has uses its self.prepended method to open up the
target class (with class_eval), and defines the method right there on
the target class (as well as aliasing the old render method). This
monkeypatching is exactly the sort of thing prepend is supposed to help
you get away from.

The monkeypatching can cause serious issue if any other gems are
prepending to ActionController::Base, since those gems will (hopefully)
use the new prepend pattern and call super to delegate to the original
implementation, but super now calls the version of render defined in
wicked's class_eval. That render then calls render_without_wicked_pdf,
which is now aliased to any render override that has been prepended
before wicked, and you get an infinite loop.
@vlymar vlymar assigned vlymar and ajb and unassigned vlymar and ajb Sep 15, 2016
@vlymar
Copy link
Author

vlymar commented Sep 15, 2016

un-assigning you adam because i want to draft the actual PR text and have you look at it. feel free to look at code now though

@vlymar
Copy link
Author

vlymar commented Sep 15, 2016

just realized my change to .prepended likely broke the alias method chain in .included

@vlymar
Copy link
Author

vlymar commented Sep 19, 2016

so I found a way simpler way to fix their monkeypatch for both prepend and include. Also added a description for the PR, and added an explanatory comment to the test.

If the maintainers don't want to merge this in, i suppose we can try do the prepend the wrong way in our fortitude fork as well to avoid having to maintain 2 forks

end

def render_with_wicked_pdf(options = nil, *args, &block)
if options.is_a?(Hash) && options.key?(:pdf)
log_pdf_creation
options[:basic_auth] = set_basic_auth(options)
make_and_send_pdf(options.delete(:pdf), (WickedPdf.config || {}).merge(options))
else
elsif respond_to?(:render_without_wicked_pdf)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. What determines whether this returns true or false?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PdfHelper is included, this method gets created by alias_method_chain. If its prepended, this method does not exist

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah jeez, I hadn't seen their railtie.rb. Makes sense now.

@ajb
Copy link

ajb commented Sep 19, 2016

Let me test my Rails 5 branch with this version of wicked_pdf and our Fortitude... but this looks 👍 to me. I'm sure the wicked_pdf maintainers will be better suited to giving feedback.

@ajb
Copy link

ajb commented Sep 19, 2016

This works as expected in our Rails 5 branch. Gonna merge this, and 👍 on you submitting upstream.

@ajb ajb merged commit 1eb40eb into master Sep 19, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants