Merging Query Strings when Redirecting in Rails

Zachary Porter, Former Senior Developer

Article Category: #Code

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.

Related Articles