Dheeraj Kumar

Ruby, Golang, Scala, Javascript

Subdomains With Go

| Comments

Go has a pretty decent standard library. While it isn’t as extensive as Ruby’s, it certainly covers several requirements a developer might have. In this post, I’ll talk about net/http and subdomains.

In our product EngagementHQ, every customer purchases a site for their community. These sites can be accessed by one or more top-level/sub domains associated with them, but to begin with, we provide them a subdomain so that they can customize their site. These are usually of the format http://sitename.engagementhq.com.

Our app is built using Ruby on Rails, and this is a simplified version of our implementation of multitenancy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ApplicationController < ActionController::Base

  prepend_before_filter :set_site

  def set_site
    # `domains` is a cache with all the domains we can expect requests from.
    site_id = domains[request.host.downcase]
    @current_site = Site.find_by(id: site_id)

    if !@current_site
      # Handle 404
    end
  end
end

While building an app with Go recently, I wanted to do the same. I started out with a simple HTTP server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", r.URL.Path[1:])
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

I then used a Pow domain as a proxy to port 8080. So when I visited http://hello.dev, I could access the app. However, when I visited http://tenant1.hello.dev, the server threw a 404.

The first step in figuring this out was to read the net/http docs. It talks about the second parameter to the ListenAndServe call, and why it’s nil. Go provides a HTTP router/multiplexer by default, appropriately named DefaultServeMux, which does not serve subdomains.

The solution is to create a new ServeMux, and make the server use it to resolve any incoming connections. This is how I did it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Mux struct {
  http.Handler
}

func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  mux.ServeHTTP(w, r)
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", handler)

  http.ListenAndServe(":8080", mux)
}

Note that http.Handler is an interface, so it cannot be used directly as a receiver, and needs to be implemented in a type.

Now, any check we need to perform about the host should go into the ServeHTTP function. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var subdomains := ... // Load list from the database/cache

func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  domainParts := strings.Split(r.Host, ".")

  if subdomain := subdomains[domainParts[0]]; subdomain != nil {
    ... // Store the subdomain in a session for future use.

    mux.ServeHTTP(w, r)
  } else {
    // Handle 404
    http.Error(w, "Not found", 404)
  }
}

Another useful trick here is to use the same port to serve multiple parts of the apps using different multiplexers:

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
type Subdomains map[string]http.Handler

func (subdomains Subdomains) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  domainParts := strings.Split(r.Host, ".")

  if mux := subdomains[domainParts[0]]; mux != nil {
    // Let the appropriate mux serve the request
    mux.ServeHTTP(w, r)
  } else {
    // Handle 404
    http.Error(w, "Not found", 404)
  }
}

func main() {
  adminMux := http.NewServeMux()
  adminMux.HandleFunc("/admin/pathone", adminHandlerOne)
  adminMux.HandleFunc("/admin/pathtwo", adminHandlerTwo)

  analyticsMux := http.NewServeMux()
  analyticsMux.HandleFunc("/analytics/pathone", analyticsHandlerOne)
  analyticsMux.HandleFunc("/analytics/pathtwo", analyticsHandlerTwo)

  subdomains := make(Subdomains)
  subdomains["admin"] = adminMux
  subdomains["analytics"] = analyticsMux

  http.ListenAndServe(":8080", subdomains)
}

One definitely has to do more work with Go than Rails, but it’s quite cool to have less magic in my codebase, and more nuts & bolts. I had fun figuring this out, and I hope someone finds this useful!

Comments