Merging Query Strings when Redirecting in Rails
Zachary Porter, Former Senior Developer
Article Category:
Posted on
The Rails router provides a redirect method, but it doesn't include the request query string. Let's fix that.
Note: the following was tested with Ruby 2.3.1p112 / Rails 5.0.0. Your mileage may vary depending on versions.
Using Out-of-the-Box Redirect
The Rails router conveniently provides a redirect
method for redirecting. An example of redirecting a request for /kittens
to a DuckDuckGo image search for the delightful little critters would look like:
# config/routes.rb
Rails.application.routes.draw do
get 'kittens',
to: redirect('https://duckduckgo.com/?q=kittens&iax=1&ia=images')
end
Pretty simple, right? Right. But now the analytics guy is asking why the Google Tag Manager query string parameters in the request are not being carried over to the destination. Visiting /kittens?gtm_a=123
yields a redirect to the DuckDuckGo page, but the gtm_a
query string parameter didn't make it across. Oh no.
Using Block Syntax #
According to the Rails Guides on routing redirection, a block can be passed into the redirect
method. Using that to merge the request query string parameters with the destination query string would look like:
Rails.application.routes.draw do
get 'puppies', to: redirect { |params, request|
response_query = Rack::Utils.parse_query('q=puppies&iax=1&ia=images')
request_query = Rack::Utils.parse_query(request.query_string)
query = request_query.merge(response_query)
"https://duckduckgo.com/?#{query.to_query}"
}
end
On line 2, a redirect for /puppies
is defined using this syntax. The block takes 2 arguments: the path params
from the URL (e.g. the :id
bit in /puppies/:id
), and the ActionDispatch request
object. The destination URL doesn't use path parameters, so params
is ignored for now. On lines 3 and 4, the Rack::Utils#parse_query
method is used to convert the request and response (destination) query strings into Hash objects. On line 5, the response query is merged with the request query, ensuring that if there are any shared keys between the two, the key/value pairs in the response take precedence. Lastly, the destination URL with the merged query string tacked on the end is returned. With this in place, a visit to /puppies?gtm_a=123
yields a redirect to https://duckduckgo.com/?gtm_a=123&ia=images&iax=1&q=puppies
. Great!
Now, to prove that the query strings are being merged properly (with the destination query values taking precendence), visiting /puppies?gtm_a=123&q=snakes
yields a redirect to https://duckduckgo.com/?gtm_a=123&ia=images&iax=1&q=puppies
. Everything appears to be working as expected. Success!
Using a Ruby Object #
Now, another redirect for /ducklings
needs added with the same query merging logic. We could copy and paste the block for puppies, tweak a couple of values, and call it a day. But Rails provides us with a better way. Let's take a look:
require 'query_fusion_redirector'
Rails.application.routes.draw do
get 'ducklings',
to: redirect(
QueryFusionRedirector.new('https://duckduckgo.com/?q=ducklings&iax=1&ia=images')
)
end
Line 1 explicitly requires a new Ruby class named QueryFusionRedirector
, which can be found in lib/query_fusion_redirector.rb
. Line 4 defines the /ducklings
route, and line 5 sets it to redirect to a DuckDuckGo search result page for adorable duckling images. Using the Ruby class here has the same results as the block above and has the added bonus of being nice and reusable, but what's going on inside of it?
# lib/query_fusion_redirector.rb
class QueryFusionRedirector
attr_reader :response_uri
def initialize(url)
@response_uri = URI(url)
end
def call(_params, request)
request_query = parse_query(request.query_string)
query = request_query.merge(response_query)
response_uri.query = query.to_query.presence
response_uri.to_s
end
private
def response_query
parse_query(response_uri.query)
end
def parse_query(query_string)
Rack::Utils.parse_query(query_string)
end
end
The initialize
method on line 6 takes a destination redirect URL. That URL gets cast as a URI
object and set in a response_uri
instance variable. Casting as URI
will allow easy access to the query string. The other 2 methods (response_query
and parse_query
) are just helper methods to make the call
method a bit shorter and easier to read.
The only method required by the router's redirect
method is the call
method on line 10. The call
method takes 2 parameters: the path params
and the request
object. These parameters should look familiar as they are the same that were used in the block syntax. The call
method must return a String (line 15). The rest of the method is the same as the block syntax; merging the request and response query strings together.
There isn't much to the class and now it can be used to merge the query string values for all of the redirects going forward.
Bonus Round: Using a Class Method #
To shorten the code even further, a class-level method can be added to the router for convenience:
require 'query_fusion_redirector'
Rails.application.routes.draw do
def self.fuse_query_redirect(destination)
redirect(QueryFusionRedirector.new(destination))
end
get 'bunnies',
to: fuse_query_redirect('https://duckduckgo.com/?q=bunnies&ia=images&iax=1')
end
Line 5 defines a fuse_query_redirect
class method on the Rails router, taking a destination URL and invoking the router's redirect
method with the Ruby object defined above. Line 9 demonstrates its use with a new /bunnies
redirect to cute bunnie images.
Closing #
I hope this article provided a little more insight into the Rails router's redirect
method and helped with merging request and response query strings. If you've ever extended this method or have some neat Rails router tips, please share your experiences in the comments below.