Overactive Vocabulary

When In Doubt, Ameliorate

RSS

The Stitching Proxy

As part of redesigning spreedly.com to highlight our focus on our gateway API product (usually known affectionately as “Core”), we wanted to move the actual serving of the marketing parts of the site to a more appropriate app - initially our account management service, and eventually a single responsibility content-centric service.

Only one problem: spreedly.com was currently handled by our Subscriptions application, and we didn’t want to change either the API urls or the hosted payment page urls for that service. And it’s not just a temporary problem, either: we’re not discontinuing the Subscriptions product, and while we may eventually move it to its own subdomain, that will be a slow and careful transition that could easily last over a year.

So we were clearly going to have to serve up two different applications off of the same domain. The plan was to continue defaulting urls to the Subscriptions app, but to hijack a specific set of pages and paths and point them at the new app. I worked up a list of paths that should point at the new path, and our handsome and talented devop John worked out how to do the mapping in our frontend load balancer. The production setup actually ended up being super simple since we already use nginx proxying to unicorn, and it was simply a matter of having a particular set of paths proxy to a different backend. And because there’s no special infractructure to support it there’s also no performance overhead to sharing the domain.

But… what about when we’re developing locally? While I’d eventually like to see us using Boxen or something like it to closely mirror the production setup in our local development environments, we’re a ways off from that yet. Today we use the awesome pow to handle our local development serving, and while it does all kinds of cool things, it won’t serve multiple apps on the same domain.

I wanted to get something going quickly, which meant I needed a simple solution that integrated with our existing setup with a minimum of fuss. Enter rack-proxy. This handy tool lets you create a piece of Rack middleware that will proxy the whole request over to another app of your choosing. Using the excellent Rails guide about Rack and a nice template in a Stack Overflow answer, I quickly had something up and running:

config/initializers/stitcher.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
if ENV["STITCH"] == "1"
  require "rack-proxy"

  class Stitcher < Rack::Proxy
    EXACT = %w(/ /terms /privacy)
    PREFIX = %w(/pricing /about /support /gateways /assets)

    def initialize(app)
      @app = app
    end

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

    def rewrite_env(env)
      request = Rack::Request.new(env)

      return env unless(request.host == ENV["PUBLIC_FULL_DOMAIN"])

      unless(
          EXACT.include?(request.path) ||
          EXACT.include?(request.path + "/") ||
          PREFIX.include?(request.path) ||
          PREFIX.any?{|prefix| request.path.starts_with?(prefix + "/")})
        env["HTTP_HOST"] = ENV["SUBSCRIPTIONS_INTERNAL_DOMAIN"]
      end

      env
    end

    def extract_http_request_headers(env)
      headers = super
      headers["Host"] = ENV["PUBLIC_FULL_DOMAIN"]
      headers
    end
  end

  Rails.application.middleware.insert_before ActionDispatch::Static, Stitcher
end

Lets walk through this real quick; it’s pretty straightforward, but there are a few nuances I want to highlight. First of all, you’ll notice this only runs if a particular ENV key is set. Doing so leverages our extensive use of environment variables for app configuration, and ensures that this will only run in development and never in production. Eventually I’d like to write more about the benefits we’ve seen from switching almost exclusively to configuration via environment variables, but for now you can just note that they’re used in multiple places in this little proxy to pull the right value.

Next, note that I had to overwrite #call - this is necessary to allow passing through unproxied requests to the “hosting” application. It’d be nice if rack-proxy provided a way to do this out of the box, but it’s simple enough to implement regardless. The decision of whether or not to proxy the request is made by looking at whether the HTTP_HOST for the request has changed; if it has, then we want this request to go to the proxied app. Also note the call to #rewrite_response - this is a method provided by rack-proxy to allow adjusting the response before it is passed back to the browser. It’s not strictly necessary for me to call it here since its default behavior is to do nothing, but I used it multiple times during debugging to get a peek at what the proxy was doing, so it’s handy to keep it in the chain.

The magic happens in #rewrite_env, where I check against the set of exact and prefix paths I want rewritten and determine whether the request matches them. If it does, the HTTP_HOST is set to the proxied app’s host, and we’re good to go. The one other check I am doing here is that I’m only ever proxying when on the public/marketing domain, since this app actually serves a couple of subdomains and I only want to rewrite on one of them.

Finally, there’s a bit of trickery going on in #extract_http_request_headers to make everything work smoothly. Even though I’m rewriting the host that the request should go to, I don’t want the Host header that’s passed to the proxied app to be affected, since it should be as clueless about the fact that it’s being proxied as possible. rack-proxy uses this method to prep the headers for submission upstream, and so I just hook it and make my own last minute adjustment.

And then the middleware is inserted at the very beginning of the middleware stack, and BOOM! it all just works. Well, not quite: I spent hours debugging an issue with the headers, but other than that I really did have a solution working in a couple hours. And best of all, we now have a way in our local development environment to simulate the exact domain structure in production.

If you need to stitch another app into a Rack stack, I’d definitely recommend checking out rack-proxy.