25

I found this great blog post on how to use Rack::Proxy as a separate proxy app. The article explains how he uses Rack::Proxy to proxy requests to http://localhost:3000 to an app on port 3001 and requests to http://localhost:3000/api to an app on port 3002. I want to do the same thing, but I do not want to create a separate proxy app. Instead, I want my main Rails app to proxy requests to /blog to a different app.

Blog Post: http://livsey.org/blog/2012/02/23/using-rack-proxy-to-serve-multiple-rails-apps-from-the-same-domain-and-port/

John
  • 9,254
  • 12
  • 54
  • 75

5 Answers5

32

FWIW, I also just tackled this problem. Some may find the full code helpful, as I needed more than you posted:

# lib/proxy_to_other.rb
class ProxyToOther < Rack::Proxy
  def initialize(app)
    @app = app
  end

  def call(env)
    original_host = env["HTTP_HOST"]
    rewrite_env(env)
    if env["HTTP_HOST"] != original_host
      perform_request(env)
    else
      # just regular
      @app.call(env)
    end
  end

  def rewrite_env(env)
    request = Rack::Request.new(env)
    if request.path =~ %r{^/prefix|^/other_prefix}
      # do nothing
    else
      env["HTTP_HOST"] = "localhost:3000"
    end
    env
  end
end

Also:

# config/application.rb
# ...snip ...
module MyApplication
  class Application < Rails::Application
    # Custom Rack middlewares
    config.middleware.use "ProxyToOther" if ["development", "test"].include? Rails.env
#...snip....

This assumes your app you want to proxy some requests to is running on port 3001. I daresay the app you're hitting can be run on any port. This also assumes you only want to do the proxying in development and test environments, because you'll have a 'real' solution in production & staging (eg, nginx or a loadbalancer doing the proper thing).

steve
  • 3,276
  • 27
  • 25
  • 2
    This is great. If you want to proxy to an arbitrary app on the internet, you may also have to set `env["SERVER_PORT"] = 80` – Gabe Kopley Jun 26 '13 at 22:45
  • " This also assumes you only want to do the proxying in development and test environments, because you'll have a 'real' solution in production & staging (eg, nginx or a loadbalancer doing the proper thing)." - Why is that? Shouldn't I use Rack::Proxy in production? – David Lojudice Sb. Jan 07 '14 at 14:17
  • 2
    @DavidLojudiceSobrinho you certainly could, but it would be better to do that proxying with nginx/apache or a loadbalancer. You'll be using up a thread in your app to do a synchronous HTTP request to some other server. It's just not a wise use of resources. – steve Jan 09 '14 at 19:16
  • 1
    Great answer, @steve. Sorry for not putting any detail into my original answer. It has been some now since I encountered that problem and I don't even have the code base for it anymore. Looks like wbyoung also has a good answer, so to save an argument, I'll give you guys both an upvote. – John Mar 26 '14 at 05:33
  • How do I write a test for this in Rspec? – Yoni Baciu Aug 05 '14 at 21:48
  • 1
    @YoniBaciu if you wanted to write a unit spec, you would instantiate an instance of this and call its methods and then make assertions. Assuming you're using webmock, you could assert that a request to your other service was made. – steve Aug 07 '14 at 07:59
17

This is a slight change to steve's solution that uses a little less internal understanding of Rack::Proxy:

require 'rack/proxy'

class MyProxy < Rack::Proxy
  def initialize(app)
    @app = app
  end

  def call(env)
    # call super if we want to proxy, otherwise just handle regularly via call
    (proxy?(env) && super) || @app.call(env)
  end

  def proxy?(env)
    # do not alter env here, but return true if you want to proxy for this request.
    return true
  end

  def rewrite_env(env)
    # change the env here
    env["HTTP_HOST"] = "some.other.host"
    env
  end
end
wbyoung
  • 22,383
  • 2
  • 34
  • 40
3

Figured it out.

lib/proxy.rb

require 'rack-proxy'
class Proxy < Rack::Proxy
    def initialize(app)
        @app = app
    end

    def rewrite_env(env)
        # do magic in here
    end
end

config/application.rb

config.middleware.use "Proxy"
MilesStanfield
  • 4,571
  • 1
  • 21
  • 32
John
  • 9,254
  • 12
  • 54
  • 75
1

Below is even simpler code to proxy an api when you want http://localhost:3000/api/users/1 (for example) to go to the api namespace defined in routes.rb without using a proxy server program.

On production it would be something like http://api.sample.com/users/1.

lib/proxy.rb

require 'rack-proxy'

class Proxy < Rack::Proxy
   def perform_request(env)
     request = Rack::Request.new(env)
     if request.path =~ %r{^/api}
       #do nothing
     else
       @app.call(env)
     end
   end
end

config/application.rb

config.middleware.use "Proxy"

config/routes.rb

namespace :api, defaults: { format: :json },
        constraints: { subdomain: 'api' }, path: '/'  do
  scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
  resources :users, :only => [:show, :create, :update, :destroy]
end

lib/api_constraints.rb

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.sample.v#{@version}")
  end
end
mrlindsey
  • 300
  • 2
  • 12
1

I found gem rails-reverse-proxy more simpler and obvious to use (for react app):

Add a simple proxy controller:

class ProxyController < ApplicationController
  include ReverseProxy::Controller

  def index
    reverse_proxy "http://localhost:3000" do |config|
      # We got a 404!
      config.on_missing do |code, response|
        redirect_to root_url and return
      end
    end
  end
end

And add a route:

match 'static/*path' => 'proxy#index', via: [:get, :post, :put, :patch, :delete]
oklas
  • 7,935
  • 2
  • 26
  • 42