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

Execute middleware stack for requests not matching any endpoint or mount point. #53

Open
dekubu opened this issue Sep 12, 2023 · 15 comments

Comments

@dekubu
Copy link

dekubu commented Sep 12, 2023

I ran into a situation when using rack middlewares

I would like to implement

Screenshot 2023-09-13 at 00 46 14

the idea being, when I call

rack-app middlewares

``

I get. list of middleware  in the order they are used.

It would help to solve a problem I am having with middleswares not been in the correct order or specified in the wrong place,
@dekubu
Copy link
Author

dekubu commented Sep 12, 2023

@adamluzsi , What would be the best approach to get the current list of the middleware in the project and the order?

@adamluzsi
Copy link
Member

Each endpoint can possess its own middleware stack. Representing middleware in a comprehensible manner can be tricky, especially when dealing with procedures. This would benefit from thorough research and user feedback. Perhaps it's a good idea to gather insights from colleagues on this.

Personally, I tend to review the class definition to understand the middleware stack and its mount points. What is your take on it currently?

@dekubu
Copy link
Author

dekubu commented Sep 13, 2023

ok some context

0
If I do this Omniauth does not work

Screenshot 2023-09-13 at 12 32 48

howether if I do this it does

Screenshot 2023-09-13 at 12 30 30

the difference is that one is in the config.ru and one is using the middleware block.

what would have been useful was to see the order on which the middleware were being applied. In rails can see aa list when you run rails middleware

I have looked at the code in rails but right now with my level of understanding it is beyond me I am learning more everyday so will soon get it.

The point I am making is it is far from simple or minimal.

so my real question is what would be a very simple minimalist approach for just keeping tack of the order Middlewares.

I looked in https://github.com/rack-app/rack-app/blob/master/lib/rack/app/singleton_methods/middleware.rb

and found

module Rack::App::SingletonMethods::Middleware

  def middlewares(&block)
    @middlewares ||= []
    unless block.nil?
      @middlewares << block
      router.reset
    end
    @middlewares
  end

  alias middleware middlewares

  def use(*args, &block)
    middlewares{ |b| b.use(*args, &block) }
  end

  protected

  def next_endpoint_middlewares(&block)
    @next_endpoint_middlewares ||= []
    @next_endpoint_middlewares << block unless block.nil?
    @next_endpoint_middlewares
  end

end

could we add more information to the @middlewares collection and expose it?

I admit it is a naive approach , but until learn more about how this stuff works that is all I have lol .

hope this make sense

@dekubu
Copy link
Author

dekubu commented Sep 13, 2023

I was thinking, if this is something that I can tackle myself as it Is outside the scope to the project that is cool too, but would be good to know where to start!

@adamluzsi
Copy link
Member

I may not wholly grasp the issue when you mention, "Omniauth does not work." However, if I'm reading between the lines, it seems the OmniAuth middleware isn't just functioning as a simple middleware. Instead, it might be extending the routing logic. When it detects specific paths, like "/auth/:provider", it could interrupt the pipeline pattern propagation and send back a response instead of the original application.

With its Radix tree based routing strategy, Rack-app avoids executing any middleware if no matching endpoint is found. This is because middleware execution is strictly tied to endpoints.

There are a few potential workarounds:

  1. As you've found, you could directly use rack and load OmniAuth as a middleware. It's a common solution many opt for.
  2. Alternatively, you could create a basic rack handler, a class with the def call(env) method. Then, use the rack builder to pair it with the OmniAuth middleware. This rack-compliant instance can then be mounted to the rack-app class. If a path prefix matches the mounting point, the radix tree-based routing will hand over the request to this rack-compliant object.
  3. Another possibility is to enhance the rack-app framework to run middleware even without a matching endpoint. For instance, based on the request URL path, it could execute middleware up to the last recognized rack-app controller. I need time to investigate use cases with other users to see if this behavior is desired.

For now, if you have a middleware that doesn't tie directly to the endpoint but functions more like a hijacker, I'd recommend config.ru or mounting a rack-compliant application with that middleware. I hope this offers some clarity and support in navigating the situation. I get back to you after doing some user interviews with other rack-app users who are acquaintances.

@adamluzsi adamluzsi changed the title Listing middleware Execute middleware stack for requests not matching any endpoint or mount point. Sep 13, 2023
@adamluzsi
Copy link
Member

Typically, OmniAuth's endpoints are better suited for a rack application rather than a middleware. However, not all frameworks support mounting, which is likely why the OmniAuth team chose to implement their solution through middleware hijacking rather than creating a mountable OmniAuth controller application.

@adamluzsi
Copy link
Member

@thilonel, I'd appreciate your input on this. Do you have any use-cases that might be impacted if we change the behavior such that the middleware stack still executes, even if an endpoint results in a 404?

@dekubu
Copy link
Author

dekubu commented Sep 14, 2023

@adamluzsi first of all thanks again for the fantastic explanation , I appreciate the time as it is really helping my learning.

Now looking back on when I created this ticket I wish I would have used anything apart from OmniAuth as an example because after looking into what you mentioned above I can see how does work differently to other middleware.

The core issue is actually more about visibility. From my research I found that the order of which a middleware is included matters

The issues I was actually highlighting was that if Included the OmniAuth middleware in the config ru it worked because it was earlier in the stack than the app.

It took me a long while solving my problem ( putting the OmniAuth in confi.ru) because I do not have visibility on what is in the middleware stack and in what order they will be applied. Is there minimalist way of achieving that visibility?

@adamluzsi
Copy link
Member

Would having a detailed list of all middlewares actually assist in this situation?

If such a feature were available, it would essentially confirm what you've already observed: OmniAuth is in your middleware stack.

For a quick check, consider using Ruby's pretty printer:

require 'pp'

# At the end of your Rack::App class:
pp middlewares

This would reinforce what you've identified in your class: the presence of the middleware.

In light of this, I feel our initial analysis remains accurate. The challenge doesn't lie in middleware visibility, but in rack-app's inability to execute middleware when the requested endpoint isn't recognized. OmniAuth operates based on this premise, relying on unknown paths to execute its internal logic.

I hope this sheds some light on the situation.

@dekubu
Copy link
Author

dekubu commented Sep 15, 2023

I tried the above and it worked and I got the information I need

What was also cool , was I went through looking at the CLI and I found a really interesting switch

Screenshot 2023-09-15 at 14 28 26

it list all the routes and the Middlewares

Screenshot 2023-09-15 at 14 38 46

I can get all the information I need from here to answer the question I had above! if you play with the order of the middleware you can see it reflected.

Sooo thanks for the speedy reply and help!

@dekubu
Copy link
Author

dekubu commented Sep 15, 2023

I can see that using OmnIAuth was the wrong example to use because it was not like the other middleware, but your explanation helped and this has been fantastic learning exercise.

@adamluzsi
Copy link
Member

However, I think the core issue persists, related to middleware that extends the routing logic with paths unbeknownst to your application, correct?

It's amusing, but I genuinely overlooked the options of the routes CLI command helper. 😅
I typically rely on reading the class definition. The last time I used the CLI was to list the middleware, aiming to understand an endpoint's middleware stack after multiple mountings and embeddings between controllers.

Usage: rack-app routes [options]

list the routes for the application

    -v, --verbose                    print out everything
    -d, --desc, --description        include endpoint descriptions too
    -l, --location                   show endpoint definition locations
        --source-location
    -m, --middlewares                show endpoint definition middleware stack
    

@dekubu
Copy link
Author

dekubu commented Sep 16, 2023

Ok just reread the above and looked further and like light switch it all make sense. Yes the problem still exists.

the core issue persists, related to middleware that extends the routing logic with paths unbeknownst to your application

If I understand this correctly

Another possibility is to enhance the rack-app framework to run middleware even without a matching endpoint. For instance, based on the request URL path, it could execute middleware up to the last recognized rack-app controller.

Instead of change to rack app , could we use an adapter pattern approach, a rack app/middleware/adaapter ?
idea being the OmniAuth middleware is adapted to work with the existing rack-app stack without changing rack-app.

I have used and looked through lots of alternative frameworks based on rack and I settled on rack-app because for me it has the perfect blend of simplicity and power with a code base that I can actually read and understand.So I do understand the resistance to change it as it has been stable for so long.

I will admit I am coming from a place of ignorance so my idea could be missing the mark so any feedback negative or positive will help and will be appreciated.

@adamluzsi I look forward to see what approach you take.

@adamluzsi
Copy link
Member

@dekubu, thank you for being open. Remember, we're equal partners here. I value your input, but there's no need to be hard on yourself. Everyone has unique knowledge to offer. So, let's have a great time collaborating and supporting each other. :)

In response to your proposal, I think implementing an adapter pattern approach is quite feasible.
Here's a potential outline of the steps:

  • Construct a Rack::Builder.
    • Within the rack builder, incorporate the OmniAuth middleware.
    • In the rack builder, include a lambda handler for the "run" method to handle "not found" scenarios.
  • Provide this Rack::Builder, or the outcome of its to_app, to your rack-app controller's mount method.
class MyApp < Rack::App

  oauth = Rack::Builder.new do
    use OmniAuth...
    
    run(lambda { |env|
       [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
     })
  end

  mount oauth, to: "/oauth"

end

@adamluzsi
Copy link
Member

@thilonel, if you find the time, could you identify some scenarios where executing the middleware stack in cases of endpoint not being found might have a negative impact on applications? If no significant problems emerge from this, it seems we would be in a good position to proceed.

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

No branches or pull requests

2 participants