omniauth icon indicating copy to clipboard operation
omniauth copied to clipboard

`Rack::Builder#to_app` is not designed to be called for every request.

Open ioquatix opened this issue 2 years ago • 18 comments

https://github.com/omniauth/omniauth/blob/9cd7aad3a0bd0f27a6de81973a17722e09d4a7d2/lib/omniauth/builder.rb#L44

It can have less than ideal performance implications.

ioquatix avatar Aug 23 '22 12:08 ioquatix

Thanks for the report, this is an area of the library I haven't looked much into. Would memoizing the to_app call be sufficient?

BobbyMcWho avatar Aug 23 '22 12:08 BobbyMcWho

I think you'd want to do it at build time because memoization is not thread safe.

ioquatix avatar Aug 23 '22 21:08 ioquatix

It's also not necessary to redefine #call since it's the same as the superclass.

https://github.com/rack/rack/blob/ab9cd7416c1b5253cd280d75704bafd87510c120/lib/rack/builder.rb#L261-L263

If OmniAuth::Builder is intended to be used as shown in the README, maybe overridding ::new would be a low-impact way of resolving this?

module OmniAuth
  class Builder < ::Rack::Builder
    def self.new(default_app = nil, &block)
      builder = super
      block ? builder.to_app : builder
    end
  end
end

speckins avatar Sep 23 '22 21:09 speckins

module OmniAuth
  module Builder
    def self.new(default_app = nil, &block)
      ::Rack::Builder.new(default_app, &block).to_app
    end
  end
end

maybe consider this???

ioquatix avatar Sep 23 '22 22:09 ioquatix

Except that OmniAuth::Builder adds a few methods of its own, of which, the #provider method is probably the main reason for using it.

https://github.com/omniauth/omniauth/blob/7d90ba21c26299df8001684cbfbb6c54ce8ea440/lib/omniauth/builder.rb#L29

What about this:

module OmniAuth
  class Builder < Rack::Builder
    def initialize default_app = nil, &block
      super
      @app = to_app if default_app
    end

    def call env
      (@app || to_app).call(env)
    end

    # The rest same as current OmniAuth::Builder class,
    # not duplicated here....
  end
end

It ought to cover people who are currently using it in both ways, as middleware as suggested in the docs or as Rack::Builder is expected to be used.

speckins avatar Sep 24 '22 17:09 speckins

I don't think side effects in #initialize are a good idea.

I like your original proposal: https://github.com/omniauth/omniauth/issues/1083#issuecomment-1256684769

I'd even suggest just doing it like this:

module OmniAuth
  module Builder
    def self.build(default_app = nil, &block)
      new(default_app, &block).to_app
    end

    # All the existing code.
  end
end

Then use OmniAuth::Builder.build(...).

ioquatix avatar Sep 24 '22 22:09 ioquatix

I'm not opposed to a major bump if need be, so the change can be as breaking as needed

BobbyMcWho avatar Sep 24 '22 22:09 BobbyMcWho

The problem is that omniauth has been telling people to use OmniAuth::Builder as middleware. E.g.,

use OmniAuth::Builder do
  provider :developer
end

Any Rack-like use(middleware, app, *args, &block) implementation is going to push those arguments onto a stack, and when it finally assembles the Rack app, it's going to call OmniAuth::Builder.new(app, *args, &block). So that means that OmniAuth has no control over which class method is called, and the only two places to fix this in existing applications written this way are ::new or #initialize. (And Rack::Builder already has a class method, ::app, that calls #to_app.)

What about...

module OmniAuth
  module Builder
    def self.new(app, *args, &block)
      builder = Rack::Builder.new(app).extend(self)
      builder.instance_eval(&block) if block_given?
      builder.to_app
    end

    def provider(klass, *args, **opts, &block)
      # ...
    end

    def options(options = false)
      # ...
    end

    # ...etc
  end
end

This would work for applications using OmniAuth::Builder as middleware with no changes needed. It would break for people (like me) who have been subclassing Builder, but at least it wouldn't be subtle (TypeError (superclass must be a Class (Module given))) and could be resolved by subclassing Rack::Builder and including the OmniAuth::Builder module.

It would break for people who have been instantiating Builder and using the instance directly, though.

speckins avatar Sep 26 '22 14:09 speckins

One more option -- rename the current Builder class to something else and define a new Builder to use it internally.

module OmniAuth
  class AuthBuilder < Rack::Builder
    # ...same as current OmniAuth::Builder, except no #call

    undef_method :call
  end

  class Builder
    def initialize app, &block
      @app = AuthBuilder.app(app, &block)
    end

    def call env
      @app.call(env)
    end
  end
end

speckins avatar Sep 26 '22 14:09 speckins

If you had a preference on how you wanted this resolved (or the priorities to take into consideration, e.g., what level of backward-compatibility, etc), I'd be willing to take a crack at it and submit a PR.

speckins avatar Oct 03 '22 19:10 speckins

I think the biggest downstream consumer is devise, so we'd wanna look if/how devise uses the builder and try and at least keep backwards compatibility for that

BobbyMcWho avatar Oct 03 '22 19:10 BobbyMcWho

If you want an easy way to test external dependencies, you might like https://github.com/ioquatix/bake-test-external

ioquatix avatar Oct 03 '22 21:10 ioquatix

I searched the devise code. They don't use Builder. The only reference to "builder" is a form builder. I also ran their test suite against the code in the PR just in case, and everything passes.

speckins avatar Oct 04 '22 12:10 speckins

I opened another PR, with the Builder-as-a-module idea. I don't really have a preference between them, but the module version means there won't be an extra do-nothing piece of middleware in the stack since it just returns the built app.

speckins avatar Oct 11 '22 13:10 speckins

Thanks @speckins I will take a look as soon as I have some time, I've been very busy

BobbyMcWho avatar Oct 11 '22 13:10 BobbyMcWho

I tripped on this earlier today. How can I help get #1094 merged?

jcmfernandes avatar Jan 16 '23 20:01 jcmfernandes

I've just not had the time to manage a large refactor at the moment, I'd love to be able to take care of a few different long standing issues w/ omniauth in the next major release, I just haven't had the time to work on OmniAuth outside of critical maintenance. Namely I'd like to resolve this issue, completely remove the hashie dependency, not dup the entire strategy w/ every request, and shortcircuit out of omniauth early when the request is not on a path that omniauth cares about.

BobbyMcWho avatar Jan 16 '23 20:01 BobbyMcWho

I've just not had the time to manage a large refactor at the moment, I'd love to be able to take care of a few different long standing issues w/ omniauth in the next major release, I just haven't had the time to work on OmniAuth outside of critical maintenance. Namely I'd like to resolve this issue, completely remove the hashie dependency, not dup the entire strategy w/ every request, and shortcircuit out of omniauth early when the request is not on a path that omniauth cares about.

Makes sense! What do you think of doing those changes incrementally? I can help.

jcmfernandes avatar Jan 17 '23 18:01 jcmfernandes