Skip to content

Loading…

Calling request.params in a constraint class causes subsequent routes to be broken #2510

Closed
joevandyk opened this Issue · 19 comments

5 participants

@joevandyk

joevandyk@f5a9d65 is a failing test case.

class RouteMatcher
  def self.matches? request
    request.params 
    return false
  end
end

Routes::Application.routes.draw do
  match '/:id' => 'categories#show', :constraints => RouteMatcher

  # For a URL of /about, params[:id] will == 'about'
  # You'd expect params[:path] == 'about'
  # If you remove 'request.params' up above, it behaves as expected.
  match '*path' => 'pages#find' 
end
@joevandyk

A possibly related bug that was fixed last year: https://rails.lighthouseapp.com/projects/8994/tickets/5157

@franckverrot franckverrot added a commit to franckverrot/rails that referenced this issue
@franckverrot franckverrot Calling `request.params` in routing constraints cannot interfere with…
… subsequent routes. [Closes #2510]

As the same request is reused thru all the routing constraints,
accessing a memoized form of `params` would prevent the router from
redefining it with a new set of parameters (the current route
parameters).
88862a0
@pixeltrix
Ruby on Rails member

@joevandyk does request.path_parameters contain the right values?

@pixeltrix pixeltrix was assigned
@joevandyk

Yes, it does. An example:

# Routes
match '/:id' => 'categories#show', :constraints => RouteMatcher
match '/:goo', :controller => 'pages', :action => 'find'

# a request for /pages
params: {"controller"=>"categories", "action"=>"show", "id"=>"pages"}
path parameters: {:controller=>"pages", :action=>"find", :goo=>"pages"}
@nodakjones

request.path_parameters worked for me. Is this the preferred method for accessing params within a constraint? If so, should the routing guide be updated?

@joevandyk

88862a0 seems to fix the bug, as far as I can tell.

@pixeltrix
Ruby on Rails member

@cesario closed the pull request that contained 88862a0 - probably because he spotted that it was merging path_parameters, query_parameters and request_parameters every time request.params is accessed.

Looking at the Constraints module it seems as though the request is created each time matches? method is called. If so it should be possible to cache the hash in an instance variable rather than the environment hash, similar to the fix in the LightHouse ticket mentioned above. However it will have impact elsewhere so we need to check that we're not breaking something.

It may be easier to just delete the key from the environment hash after the constraint has been checked using an ensure block. What do you think @josevalim?

@adkron

We are having this same problem. We fail a constraint that has a match '*anything' route. The request is not routed there because of the constraint, but in instead routed to the correct matching route. Unfortunately the params are then equal to {'anything' => '/resource/1/foo'} We do have a failing test, but can't find where we can fix this.

We tried cloning the request object, and that doesn't help.

@adkron

We have a workaround that fixes the problem for us:

# We delete this key from the env hash to prevent params from being mangled in later routes
# See issue: https://github.com/rails/rails/issues/2510
request.env.delete('action_dispatch.request.parameters')

which basically prevents memoization of the parameters. We are unsure of the performance consequences of this workaround, and would prefer a real fix that we could use in our production code. Any other ideas or suggestions would be appreicated.

@pixeltrix
Ruby on Rails member

@adkron are you able to use path_parameters or symbolized_path_parameters in your constraint? Accessing the params hash causes it to be memoized in the environment hash. Preparing the params hash is not a trivial operation so recreating it for every constraint check would have a performance impact. If you use an arity of 2 with the block you can get at the path parameters easily:

class RoutingConstraint
  def self.matches?(params, request)
    #constraint check
  end
end

constraints(RoutingConstraint) do
  #routes
end
@adkron

No, path parameters doesn't work for us because we need the full request_parameters (e.g. an auth token). We tried using the two argument form by defining self.call(params, request) which doesn't trigger the bug with the wildcard matcher, but the parameters passed in are just the path parameters, not the full query and post parameters.

@pixeltrix
Ruby on Rails member

@adkron is it a form or query parameter? You should be able to use request.GET and request.POST in the block. They're also aliased as request.query_parameters and request.request_parameters.

@pixeltrix pixeltrix closed this
@pixeltrix
Ruby on Rails member

@adkron as I said it's impossible to fix this without rebuilding the combined params hash on every route comparison so I'm going to close it - if you can see a way that it can be done without that then please point it out and I'll have another look at it.

@adkron

Impossible is quite a big word. I love when I hear another developer say something is impossible. Hard, maybe, but impossible, no.

It doesn't matter what parameters I touch. I ended up just killing the cache every time I have to touch the params. This is a good enough work around, but it is really not ideal. It is a big piece of tribal knowledge.

Maybe there could be some way to only cache once the path is chosen?

This is still an issue.

@franckverrot franckverrot added a commit to franckverrot/rails that referenced this issue
@franckverrot franckverrot Try fixing #2510 6fd72e2
@franckverrot

@pixeltrix @adkron would something like this be an acceptable solution: https://github.com/cesario/rails/compare/2510 ?

I "simplified" the constraints check and used tap to chain the reset of the parameters.

@andhapp andhapp pushed a commit to andhapp/rails that referenced this issue
@pixeltrix pixeltrix Reset the request parameters after a constraints check
A callable object passed as a constraint for a route may access the request
parameters as part of its check. This causes the combined parameters hash
to be cached in the environment hash. If the constraint fails then any subsequent
access of the request parameters will be against that stale hash.

To fix this we delete the cache after every call to `matches?`. This may have a
negative performance impact if the contraint wraps a large number of routes as the
parameters hash is built by merging GET, POST and path parameters.

Fixes #2510.
5603050
@adkron

@cesario :thumbsup: I think that is a pretty good solution. If you add some tests for that I would totally go for it. @pixeltrix, does that meet the best of both worlds. Only reset the env when a constraint doesn't match?

@cesario I actually had some routing specs trying to drive out this very issue. I can see if I can dig them up and then we can get together on it.

@carlosantoniodasilva carlosantoniodasilva pushed a commit to carlosantoniodasilva/rails that referenced this issue
@pixeltrix pixeltrix Reset the request parameters after a constraints check
A callable object passed as a constraint for a route may access the request
parameters as part of its check. This causes the combined parameters hash
to be cached in the environment hash. If the constraint fails then any subsequent
access of the request parameters will be against that stale hash.

To fix this we delete the cache after every call to `matches?`. This may have a
negative performance impact if the contraint wraps a large number of routes as the
parameters hash is built by merging GET, POST and path parameters.

Fixes #2510.
(cherry picked from commit 5603050)
7c7fb3a
@pixeltrix
Ruby on Rails member

@adkron I the end I just added an ensure section to the matches? method which resets the parameters.

@amutz amutz pushed a commit that referenced this issue
@pixeltrix pixeltrix Reset the request parameters after a constraints check
A callable object passed as a constraint for a route may access the request
parameters as part of its check. This causes the combined parameters hash
to be cached in the environment hash. If the constraint fails then any subsequent
access of the request parameters will be against that stale hash.

To fix this we delete the cache after every call to `matches?`. This may have a
negative performance impact if the contraint wraps a large number of routes as the
parameters hash is built by merging GET, POST and path parameters.

Fixes #2510.
(cherry picked from commit 5603050)
0cfa6b7
@adkron

Would it be better to only reset when a constraint fails?

@pixeltrix
Ruby on Rails member

@adkron the Constraints instance wraps the Dispatcher instance (or whatever is the target of the route). It may still return a 404 with a X-Cascade: pass header. The router will continue matching so if the constraints matched and the cache was not deleted from the environment hash then they will be stale if another route is matched.

@adkron

Thanks for the fix

@ttosch ttosch pushed a commit that referenced this issue
@pixeltrix pixeltrix Reset the request parameters after a constraints check
A callable object passed as a constraint for a route may access the request
parameters as part of its check. This causes the combined parameters hash
to be cached in the environment hash. If the constraint fails then any subsequent
access of the request parameters will be against that stale hash.

To fix this we delete the cache after every call to `matches?`. This may have a
negative performance impact if the contraint wraps a large number of routes as the
parameters hash is built by merging GET, POST and path parameters.

Fixes #2510.
(cherry picked from commit 5603050)
45a5a2d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.